hof 23.0.2-vite-sourcemap-beta → 23.0.3

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 (40) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +96 -5
  3. package/build/tasks/vite/index.js +6 -7
  4. package/build/tasks/vite/vite.config.js +1 -27
  5. package/components/amount-with-unit-select/fields.js +15 -0
  6. package/components/amount-with-unit-select/hooks.js +168 -0
  7. package/components/amount-with-unit-select/index.js +107 -0
  8. package/components/amount-with-unit-select/templates/amount-with-unit-select.html +20 -0
  9. package/components/amount-with-unit-select/utils.js +197 -0
  10. package/components/amount-with-unit-select/validation.js +175 -0
  11. package/components/index.js +1 -0
  12. package/controller/controller.js +5 -3
  13. package/controller/validation/index.js +1 -1
  14. package/controller/validation/validators.js +0 -1
  15. package/frontend/template-mixins/mixins/template-mixins.js +55 -5
  16. package/frontend/template-mixins/partials/forms/grouped-inputs-select.html +13 -0
  17. package/frontend/template-mixins/partials/forms/grouped-inputs-text.html +37 -0
  18. package/frontend/themes/gov-uk/styles/_grouped-input.scss +5 -0
  19. package/frontend/themes/gov-uk/styles/govuk.scss +1 -0
  20. package/frontend/toolkit/assets/javascript/form-focus.js +4 -0
  21. package/lib/sessions.js +18 -7
  22. package/package.json +9 -4
  23. package/sandbox/apps/sandbox/fields.js +18 -1
  24. package/sandbox/apps/sandbox/index.js +4 -0
  25. package/sandbox/apps/sandbox/sections/summary-data-sections.js +7 -1
  26. package/sandbox/apps/sandbox/translations/src/en/fields.json +10 -0
  27. package/sandbox/apps/sandbox/translations/src/en/pages.json +3 -0
  28. package/sandbox/apps/sandbox/translations/src/en/validation.json +12 -0
  29. package/sandbox/server.js +0 -8
  30. package/utilities/autofill/index.js +169 -145
  31. package/frontend/govuk-template/govuk_template_generated.html +0 -118
  32. package/sandbox/apps/sandbox/translations/en/default.json +0 -245
  33. package/sandbox/public/css/app.css +0 -11708
  34. package/sandbox/public/css/app.css.map +0 -1
  35. package/sandbox/public/images/govuk-logo.svg +0 -25
  36. package/sandbox/public/images/icons/icon-caret-left.png +0 -0
  37. package/sandbox/public/images/icons/icon-complete.png +0 -0
  38. package/sandbox/public/images/icons/icon-cross-remove-sign.png +0 -0
  39. package/sandbox/public/js/bundle.js +0 -60
  40. package/sandbox/public/js/bundle.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hof",
3
3
  "description": "A bootstrap for HOF projects",
4
- "version": "23.0.2-vite-sourcemap-beta",
4
+ "version": "23.0.3",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
@@ -52,6 +52,7 @@
52
52
  "dialog-polyfill": "^0.5.6",
53
53
  "dotenv": "^4.0.0",
54
54
  "duplexify": "^3.7.1",
55
+ "exorcist": "^2.0.0",
55
56
  "express": "^4.21.2",
56
57
  "express-healthcheck": "^0.1.0",
57
58
  "express-partial-templates": "^0.2.1",
@@ -97,7 +98,7 @@
97
98
  "devDependencies": {
98
99
  "@cucumber/cucumber": "^7.3.2",
99
100
  "@cucumber/pretty-formatter": "^1.0.1",
100
- "@types/jest": "^26.0.24",
101
+ "@types/jest": "^30.0.0",
101
102
  "@vitest/coverage-v8": "^4.0.8",
102
103
  "@xmldom/xmldom": "~0.8.11",
103
104
  "chai": "^3.5.0",
@@ -110,7 +111,8 @@
110
111
  "funkie": "0.0.5",
111
112
  "funkie-phantom": "0.0.1",
112
113
  "istanbul": "^0.4.5",
113
- "jest": "^26.6.3",
114
+ "jest": "^30.2.0",
115
+ "jest-environment-jsdom": "^30.2.0",
114
116
  "jquery": "^3.7.1",
115
117
  "mocha": "^8.4.0",
116
118
  "mocha-sandbox": "^1.0.0",
@@ -127,7 +129,10 @@
127
129
  "travis-conditions": "0.0.0",
128
130
  "vitest": "^4.0.8",
129
131
  "watchify": "^4.0.0",
130
- "webdriverio": "^4.14.4"
132
+ "webdriverio": "^5.0.0"
133
+ },
134
+ "jest": {
135
+ "testEnvironment": "jsdom"
131
136
  },
132
137
  "mocha": {
133
138
  "reporter": "spec",
@@ -3,6 +3,7 @@
3
3
 
4
4
  const dateComponent = require('../../../').components.date;
5
5
  const staticAppealStages = require('./lib/staticAppealStages');
6
+ const amountWithUnitSelectComponent = require('../../../').components.amountWithUnitSelect;
6
7
 
7
8
  module.exports = {
8
9
  'landing-page-radio': {
@@ -124,5 +125,21 @@ module.exports = {
124
125
  value: '',
125
126
  label: 'fields.appealStages.options.null'
126
127
  }].concat(staticAppealStages.getstaticAppealStages())
127
- }
128
+ },
129
+ 'amountWithUnitSelect' : amountWithUnitSelectComponent('amountWithUnitSelect', {
130
+ mixin: 'input-amount-with-unit-select',
131
+ amountLabel: "Amount-",
132
+ unitLabel: "Unit-",
133
+ options: [
134
+ { "null": "Select" },
135
+ { "label": "non trans option 1", "value": "1" },
136
+ { "label": "non trans option 2", "value": "2" }
137
+ ],
138
+ hint: "E.G: 5 Kilogram",
139
+ legend: 'Enter An Amount',
140
+ isPageHeading: 'true',
141
+ unitOptional: 'false',
142
+ amountOptional: 'true',
143
+ validate: ['alphanum']
144
+ })
128
145
  }
@@ -31,6 +31,10 @@ module.exports = {
31
31
  '/dob': {
32
32
  fields: ['dateOfBirth'],
33
33
  locals: { showSaveAndExit: true },
34
+ next: '/amount-unit'
35
+ },
36
+ '/amount-unit':{
37
+ fields: ['amountWithUnitSelect'],
34
38
  next: '/address'
35
39
  },
36
40
  '/address': {
@@ -39,5 +39,11 @@ module.exports = {
39
39
  ],
40
40
  whatHappened: [
41
41
  'whatHappened'
42
- ]
42
+ ],
43
+ amountWithUnitSelect:
44
+ [{
45
+ field: 'amountWithUnitSelect',
46
+ parse: val => val ?
47
+ (val.substring(0, val.lastIndexOf('-')) || '0') + ' ' + val.substring(val.lastIndexOf('-') + 1) : ''
48
+ }]
43
49
  };
@@ -97,5 +97,15 @@
97
97
  "options": {
98
98
  "null": "Select..."
99
99
  }
100
+ },
101
+ "amountWithUnitSelect": {
102
+ "label": "Amount",
103
+ "amountLabel": "Amount:",
104
+ "unitLabel": "Unit:",
105
+ "options": [
106
+ { "null": "Select..." },
107
+ { "label": "Litres", "value": "L" },
108
+ { "label": "Kilograms", "value": "KG" }
109
+ ]
100
110
  }
101
111
  }
@@ -45,6 +45,9 @@
45
45
  },
46
46
  "whatHappened": {
47
47
  "header": "What happened"
48
+ },
49
+ "amountWithUnitSelect": {
50
+ "header": "Entered amount"
48
51
  }
49
52
  }
50
53
  },
@@ -50,5 +50,17 @@
50
50
  },
51
51
  "appealStages": {
52
52
  "required": "Select an appeal stage from the list"
53
+ },
54
+ "amountWithUnitSelect": {
55
+ "default": "Enter the amount in the correct format; for example, 10 Litres",
56
+ "alphanum": "The amount must not contain any special characters",
57
+ "required": "Enter an amount and a unit value"
58
+ },
59
+ "amountWithUnitSelect-unit": {
60
+ "default": "A valid value must be selected as the amount unit",
61
+ "required": "A unit must be selected for the amount"
62
+ },
63
+ "amountWithUnitSelect-amount": {
64
+ "required": "An amount must be selected with the unit"
53
65
  }
54
66
  }
package/sandbox/server.js CHANGED
@@ -22,13 +22,5 @@ bootstrap({
22
22
  "port": 8082,
23
23
  session: {
24
24
  secret: require('crypto').randomBytes(16).toString('hex')
25
- },
26
- build: {
27
- sass: {
28
- sourceMaps: true
29
- },
30
- js: {
31
- sourceMaps: true
32
- }
33
25
  }
34
26
  });
@@ -3,187 +3,211 @@
3
3
 
4
4
  const url = require('url');
5
5
  const Inputs = require('./inputs');
6
- const Promise = require('bluebird');
7
6
 
8
7
  const debug = require('debug')('hof:util:autofill');
9
8
 
10
9
  const MAX_LOOPS = 3;
11
10
 
12
- module.exports = browser => (target, input, opts) => {
11
+ function getRemoteFilePath(uploadResult) {
12
+ if (uploadResult && typeof uploadResult === 'object' && uploadResult.value) {
13
+ return uploadResult.value;
14
+ }
15
+
16
+ return uploadResult;
17
+ }
18
+
19
+ module.exports = browser => async (target, input, opts) => {
13
20
  const options = opts || {};
14
21
  options.maxLoops = options.maxLoops || MAX_LOOPS;
15
-
16
22
  const getValue = Inputs(input);
17
-
18
23
  let last;
19
24
  let count = 0;
20
25
 
21
- function completeTextField(element, name) {
26
+ async function completeTextField(element, name) {
22
27
  const value = getValue(name, 'text');
23
28
  debug(`Filling field: ${name} with value: ${value}`);
24
- return browser
25
- .elementIdClear(element)
26
- .elementIdValue(element, value)
27
- .catch(() => {
28
- // any error here is *probably* because the field is hidden
29
- // ignore and hope for the best
30
- });
29
+ try {
30
+ await element.clearValue();
31
+ await element.setValue(value);
32
+ } catch (e) {
33
+ // any error here is *probably* because the field is hidden
34
+ // ignore and hope for the best
35
+ }
31
36
  }
32
37
 
33
- function completeFileField(element, name) {
38
+ async function completeFileField(element, name) {
34
39
  const value = getValue(name, 'file');
35
40
  if (value) {
36
41
  debug(`Uploading file: ${value}`);
37
- return browser.uploadFile(value)
38
- .then(response => {
39
- debug(`Uploaded file: ${value} - remote path ${response.value}`);
40
- return browser
41
- .addValue(`input[name="${name}"]`, response.value);
42
- });
42
+ const remotePath = getRemoteFilePath(await browser.uploadFile(value));
43
+ debug(`Uploaded file: ${value} - remote path ${remotePath}`);
44
+ await element.setValue(remotePath);
45
+ } else {
46
+ debug(`No file specified for input ${name} - ignoring`);
43
47
  }
44
- debug(`No file specified for input ${name} - ignoring`);
45
48
  }
46
49
 
47
- function completeRadio(element, name) {
50
+ async function completeRadioGroup(name) {
48
51
  const value = getValue(name, 'radio');
49
- if (!value) {
50
- return browser.elements(`input[type="radio"][name="${name}"]`)
51
- .then(radios => {
52
- debug(`Checking random radio: ${name}`);
53
- const index = 1 + Math.floor(Math.random() * (radios.value.length - 1));
54
- return browser.elementIdClick(radios.value[index].ELEMENT);
55
- });
52
+ const radios = await browser.$$(`input[type="radio"][name="${name}"]`);
53
+
54
+ if (!radios.length) {
55
+ debug(`No radio inputs found for ${name}`);
56
+ return;
56
57
  }
57
- return browser.elementIdAttribute(element, 'value')
58
- .then(val => {
59
- if (val.value === value) {
60
- debug(`Checking radio: ${name} with value: ${val.value}`);
61
- browser.elementIdClick(element);
58
+
59
+ if (!value) {
60
+ debug(`Checking random radio: ${name}`);
61
+ const index = Math.floor(Math.random() * radios.length);
62
+ if (!await radios[index].isSelected()) {
63
+ await radios[index].click();
64
+ }
65
+ } else {
66
+ for (const radio of radios) {
67
+ const val = await radio.getAttribute('value');
68
+ if (val === value) {
69
+ debug(`Checking radio: ${name} with value: ${val}`);
70
+ if (!await radio.isSelected()) {
71
+ await radio.click();
72
+ }
73
+ return;
62
74
  }
63
- });
75
+ }
76
+
77
+ debug(`Ignoring radio group: ${name} - no option matches ${value}`);
78
+ }
64
79
  }
65
80
 
66
- function completeCheckbox(element, name) {
81
+ async function completeCheckbox(element, name) {
67
82
  const value = getValue(name, 'checkbox');
68
- return browser.elementIdAttribute(element, 'value')
69
- .then(val => browser.elementIdAttribute(element, 'checked')
70
- .then(checked => {
71
- if (value === null) {
72
- if (!checked.value) {
73
- debug(`Leaving checkbox: ${name} blank`);
74
- return;
75
- }
76
- debug(`Unchecking checkbox: ${name}`);
77
- return browser.elementIdClick(element);
78
- }
79
- if (!value && !checked.value) {
80
- debug(`Checking checkbox: ${name} with value: ${val.value}`);
81
- return browser.elementIdClick(element);
82
- } else if (value && value.indexOf(val.value) > -1 && !checked.value) {
83
- debug(`Checking checkbox: ${name} with value: ${val.value}`);
84
- return browser.elementIdClick(element);
85
- } else if (value && value.indexOf(val.value) === -1 && checked.value) {
86
- debug(`Unchecking checkbox: ${name} with value: ${val.value}`);
87
- return browser.elementIdClick(element);
88
- }
89
- debug(`Ignoring checkbox: ${name} with value: ${val.value} - looking for ${value}`);
90
- }));
83
+ const val = await element.getAttribute('value');
84
+ const checked = await element.isSelected();
85
+ if (value === null) {
86
+ if (!checked) {
87
+ debug(`Leaving checkbox: ${name} blank`);
88
+ return;
89
+ }
90
+ debug(`Unchecking checkbox: ${name}`);
91
+ await element.click();
92
+ } else if (!value && !checked) {
93
+ debug(`Checking checkbox: ${name} with value: ${val}`);
94
+ await element.click();
95
+ } else if (value && value.indexOf(val) > -1 && !checked) {
96
+ debug(`Checking checkbox: ${name} with value: ${val}`);
97
+ await element.click();
98
+ } else if (value && value.indexOf(val) === -1 && checked) {
99
+ debug(`Unchecking checkbox: ${name} with value: ${val}`);
100
+ await element.click();
101
+ } else {
102
+ debug(`Ignoring checkbox: ${name} with value: ${val} - looking for ${value}`);
103
+ }
91
104
  }
92
105
 
93
- function completeSelectElement(element, name) {
106
+ async function completeSelectElement(element, name) {
94
107
  const value = getValue(name, 'select');
95
108
  if (!value) {
96
- return browser.elementIdElements(element, 'option')
97
- .then(o => {
98
- const index = 1 + Math.floor(Math.random() * (o.value.length - 1));
99
- debug(`Selecting option: ${index} from select box: ${name}`);
100
- return browser.selectByIndex(`select[name="${name}"]`, index);
101
- });
109
+ const selectOptions = await element.$$('option');
110
+ if (selectOptions.length > 1) {
111
+ const index = 1 + Math.floor(Math.random() * (selectOptions.length - 1));
112
+ debug(`Selecting option: ${index} from select box: ${name}`);
113
+ await element.selectByIndex(index);
114
+ }
115
+ } else {
116
+ debug(`Selecting options: ${value} from select box: ${name}`);
117
+ await element.selectByAttribute('value', value);
102
118
  }
103
- debug(`Selecting options: ${value} from select box: ${name}`);
104
- return browser.selectByValue(`select[name="${name}"]`, value);
105
119
  }
106
120
 
107
- function completeStep(path) {
108
- return browser
109
- .elements('input')
110
- .then(fields => {
111
- debug(`Found ${fields.value.length} <input> elements`);
112
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'type')
113
- .then(type => browser.elementIdAttribute(field.ELEMENT, 'name')
114
- .then(name => {
115
- if (type.value === 'radio') {
116
- return completeRadio(field.ELEMENT, name.value);
117
- } else if (type.value === 'checkbox') {
118
- return completeCheckbox(field.ELEMENT, name.value);
119
- } else if (type.value === 'file') {
120
- return completeFileField(field.ELEMENT, name.value);
121
- } else if (type.value === 'text') {
122
- return completeTextField(field.ELEMENT, name.value);
123
- }
124
- debug(`Ignoring field of type ${type.value}`);
125
- })), {concurrency: 1});
126
- })
127
- .elements('select')
128
- .then(fields => {
129
- debug(`Found ${fields.value.length} <select> elements`);
130
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'name')
131
- .then(name => completeSelectElement(field.ELEMENT, name.value)));
132
- })
133
- .elements('textarea')
134
- .then(fields => {
135
- debug(`Found ${fields.value.length} <textarea> elements`);
136
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'name')
137
- .then(name => completeTextField(field.ELEMENT, name.value)));
138
- })
139
- .then(() => {
140
- if (options.screenshots) {
141
- const screenshot = require('path').resolve(options.screenshots, 'hof-autofill.pre-submit.png');
142
- return browser.saveScreenshot(screenshot);
121
+ async function completeStep(path) {
122
+ const completedRadioGroups = new Set();
123
+
124
+ // Fill inputs
125
+ const inputs = await browser.$$('input');
126
+ debug(`Found ${inputs.length} <input> elements`);
127
+ for (const element of inputs) {
128
+ const type = await element.getAttribute('type');
129
+ const name = await element.getAttribute('name');
130
+ if (type === 'radio') {
131
+ if (!completedRadioGroups.has(name)) {
132
+ completedRadioGroups.add(name);
133
+ await completeRadioGroup(name);
143
134
  }
144
- })
145
- .then(() => {
146
- debug('Submitting form');
147
- return browser.$('input[type="submit"]').click();
148
- })
149
- .then(() => browser.getUrl()
150
- .then(p => {
151
- const u = url.parse(p);
152
- debug(`New page is: ${u.path}`);
153
- if (u.path !== path) {
154
- debug(`Checking current path ${u.path} against last path ${last}`);
155
- if (last === u.path) {
156
- count++;
157
- debug(`Stuck on path ${u.path} for ${count} iterations`);
158
- if (count === options.maxLoops) {
159
- if (options.screenshots) {
160
- const screenshot = require('path').resolve(options.screenshots, 'hof-autofill.debug.png');
161
- return browser.saveScreenshot(screenshot)
162
- .then(() => {
163
- throw new Error(`Progress stuck at ${u.path} - screenshot saved to ${screenshot}`);
164
- });
165
- }
166
- throw new Error(`Progress stuck at ${u.path}`);
167
- }
168
- } else {
169
- count = 0;
170
- }
171
- last = u.path;
172
- return completeStep(path);
135
+ } else if (type === 'checkbox') {
136
+ await completeCheckbox(element, name);
137
+ } else if (type === 'file') {
138
+ await completeFileField(element, name);
139
+ } else if (type === 'text') {
140
+ await completeTextField(element, name);
141
+ } else {
142
+ debug(`Ignoring field of type ${type}`);
143
+ }
144
+ }
145
+
146
+ // Fill selects
147
+ const selects = await browser.$$('select');
148
+ debug(`Found ${selects.length} <select> elements`);
149
+ for (const element of selects) {
150
+ const name = await element.getAttribute('name');
151
+ await completeSelectElement(element, name);
152
+ }
153
+
154
+ // Fill textareas
155
+ const textareas = await browser.$$('textarea');
156
+ debug(`Found ${textareas.length} <textarea> elements`);
157
+ for (const element of textareas) {
158
+ const name = await element.getAttribute('name');
159
+ await completeTextField(element, name);
160
+ }
161
+
162
+ if (options.screenshots) {
163
+ const screenshot = path.resolve(options.screenshots, 'hof-autofill.pre-submit.png');
164
+ await browser.saveScreenshot(screenshot);
165
+ }
166
+
167
+ debug('Submitting form');
168
+ const submitBtn = await browser.$('input[type="submit"], button[type="submit"]');
169
+ if (!await submitBtn.isExisting()) {
170
+ throw new Error('No submit control found on page');
171
+ }
172
+ await submitBtn.click();
173
+
174
+ const p = await browser.getUrl();
175
+ const u = url.parse(p);
176
+ debug(`New page is: ${u.path}`);
177
+ if (u.path !== path) {
178
+ debug(`Checking current path ${u.path} against last path ${last}`);
179
+ if (last === u.path) {
180
+ count++;
181
+ debug(`Stuck on path ${u.path} for ${count} iterations`);
182
+ if (count === options.maxLoops) {
183
+ if (options.screenshots) {
184
+ const screenshot = path.resolve(options.screenshots, 'hof-autofill.debug.png');
185
+ await browser.saveScreenshot(screenshot);
186
+ throw new Error(`Progress stuck at ${u.path} - screenshot saved to ${screenshot}`);
173
187
  }
174
- debug(`Arrived at ${path}. Done.`);
175
- }))
176
- .catch(e => browser.getText('#content')
177
- .then(text => {
178
- debug('PAGE CONTENT >>>>>>');
179
- debug(text);
180
- debug('END PAGE CONTENT >>>>>>');
181
- })
182
- .catch(() => null)
183
- .then(() => {
184
- throw e;
185
- }));
188
+ throw new Error(`Progress stuck at ${u.path}`);
189
+ }
190
+ } else {
191
+ count = 0;
192
+ }
193
+ last = u.path;
194
+ return completeStep(path);
195
+ }
196
+ debug(`Arrived at ${path}. Done.`);
186
197
  }
187
198
 
188
- return completeStep(target);
199
+ try {
200
+ await completeStep(target);
201
+ } catch (e) {
202
+ try {
203
+ const content = await browser.$('#content');
204
+ const text = await content.getText();
205
+ debug('PAGE CONTENT >>>>>>');
206
+ debug(text);
207
+ debug('END PAGE CONTENT >>>>>>');
208
+ } catch (err) {
209
+ // ignore error
210
+ }
211
+ throw e;
212
+ }
189
213
  };
@@ -1,118 +0,0 @@
1
-
2
- <!DOCTYPE html>
3
- <!--[if lt IE 9]><html class="lte-ie8" lang="{{htmlLang}}"><![endif]-->
4
- <!--[if gt IE 8]><!--><html lang="{{htmlLang}}" class="govuk-template--rebranded"><!--<![endif]-->
5
- <head>
6
- <meta charset="utf-8" />
7
- <title>{{$pageTitle}}{{/pageTitle}}</title>
8
- {{$head}}{{/head}}
9
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
10
- <meta name="theme-color" content="#1d70b8">
11
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
- <link rel="icon" sizes="48x48" href="{{govukAssetPath}}rebrand/images/favicon.ico">
13
- <link rel="icon" sizes="any" href="{{govukAssetPath}}rebrand/images/favicon.svg" type="image/svg+xml">
14
- <link rel="mask-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-mask.svg" color="#1d70b8">
15
- <link rel="apple-touch-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-180.png">
16
- <link rel="manifest" href="{{govukAssetPath}}rebrand/manifest.json">
17
- <meta property="og:image" content="{{govukAssetPath}}rebrand/images/govuk-opengraph-image.png">
18
- </head>
19
-
20
- <body class="{{$bodyClasses}}{{/bodyClasses}} govuk-template__body js-enabled" >
21
- <script {{#nonce}}nonce="{{nonce}}"{{/nonce}}>document.body.className += ' js-enabled' + ('noModule' in HTMLScriptElement.prototype ? ' govuk-frontend-supported' : '');</script>
22
-
23
-
24
- <div id="global-cookie-message" class="gem-c-cookie-banner govuk-clearfix" data-module="cookie-banner" role="region" aria-label="cookie banner" data-nosnippet="">
25
- {{$cookieMessage}}{{/cookieMessage}}
26
- </div>
27
-
28
- {{$bodyStart}}{{/bodyStart}}
29
-
30
- <header role="banner" id="govuk-header" class="{{$headerClass}}{{/headerClass}}">
31
- <div class="govuk-header__container govuk-width-container">
32
-
33
- <div class="govuk-header__logo">
34
- <a href="{{$homepageUrl}}https://www.gov.uk{{/homepageUrl}}" title="{{$logoLinkTitle}}Go to the GOV.UK homepage{{/logoLinkTitle}}" id="logo" class="govuk-header__link govuk-header__link--homepage" target="_blank" data-module="track-click" data-track-category="homeLinkClicked" data-track-action="homeHeader">
35
- <!--[if gt IE 8]><!-->
36
- <div id="govuk-header__logo"></div>
37
- <img src="/public/images/govuk-logo.svg" id="govuk-header__logo" alt="Logo" loading="lazy" />
38
- <!--<![endif]-->
39
- <!--[if IE 8]>
40
- <img src="{{govukAssetPath}}rebrand/images/govuk-logotype-tudor-crown.png" class="govuk-header__logotype-crown-fallback-image" width="32" height="30" alt="">
41
- <![endif]-->
42
- </a>
43
- </div>
44
- {{$insideHeader}}{{/insideHeader}}
45
-
46
- {{$propositionHeader}}{{/propositionHeader}}
47
- </div>
48
- </header>
49
-
50
-
51
- {{$afterHeader}}{{/afterHeader}}
52
-
53
-
54
- {{$main}}{{/main}}
55
-
56
- <footer class="govuk-footer">
57
- <div class="govuk-width-container">
58
- <svg
59
- focusable="false"
60
- role="presentation"
61
- xmlns="http://www.w3.org/2000/svg"
62
- viewBox="0 0 64 60"
63
- height="30"
64
- width="32"
65
- fill="currentcolor" class="govuk-footer__crown">
66
- <g>
67
- <circle cx="20" cy="17.6" r="3.7" />
68
- <circle cx="10.2" cy="23.5" r="3.7" />
69
- <circle cx="3.7" cy="33.2" r="3.7" />
70
- <circle cx="31.7" cy="30.6" r="3.7" />
71
- <circle cx="43.3" cy="17.6" r="3.7" />
72
- <circle cx="53.2" cy="23.5" r="3.7" />
73
- <circle cx="59.7" cy="33.2" r="3.7" />
74
- <circle cx="31.7" cy="30.6" r="3.7" />
75
- <path d="M33.1,9.8c.2-.1.3-.3.5-.5l4.6,2.4v-6.8l-4.6,1.5c-.1-.2-.3-.3-.5-.5l1.9-5.9h-6.7l1.9,5.9c-.2.1-.3.3-.5.5l-4.6-1.5v6.8l4.6-2.4c.1.2.3.3.5.5l-2.6,8c-.9,2.8,1.2,5.7,4.1,5.7h0c3,0,5.1-2.9,4.1-5.7l-2.6-8ZM37,37.9s-3.4,3.8-4.1,6.1c2.2,0,4.2-.5,6.4-2.8l-.7,8.5c-2-2.8-4.4-4.1-5.7-3.8.1,3.1.5,6.7,5.8,7.2,3.7.3,6.7-1.5,7-3.8.4-2.6-2-4.3-3.7-1.6-1.4-4.5,2.4-6.1,4.9-3.2-1.9-4.5-1.8-7.7,2.4-10.9,3,4,2.6,7.3-1.2,11.1,2.4-1.3,6.2,0,4,4.6-1.2-2.8-3.7-2.2-4.2.2-.3,1.7.7,3.7,3,4.2,1.9.3,4.7-.9,7-5.9-1.3,0-2.4.7-3.9,1.7l2.4-8c.6,2.3,1.4,3.7,2.2,4.5.6-1.6.5-2.8,0-5.3l5,1.8c-2.6,3.6-5.2,8.7-7.3,17.5-7.4-1.1-15.7-1.7-24.5-1.7h0c-8.8,0-17.1.6-24.5,1.7-2.1-8.9-4.7-13.9-7.3-17.5l5-1.8c-.5,2.5-.6,3.7,0,5.3.8-.8,1.6-2.3,2.2-4.5l2.4,8c-1.5-1-2.6-1.7-3.9-1.7,2.3,5,5.2,6.2,7,5.9,2.3-.4,3.3-2.4,3-4.2-.5-2.4-3-3.1-4.2-.2-2.2-4.6,1.6-6,4-4.6-3.7-3.7-4.2-7.1-1.2-11.1,4.2,3.2,4.3,6.4,2.4,10.9,2.5-2.8,6.3-1.3,4.9,3.2-1.8-2.7-4.1-1-3.7,1.6.3,2.3,3.3,4.1,7,3.8,5.4-.5,5.7-4.2,5.8-7.2-1.3-.2-3.7,1-5.7,3.8l-.7-8.5c2.2,2.3,4.2,2.7,6.4,2.8-.7-2.3-4.1-6.1-4.1-6.1h10.6,0Z" />
76
- </g>
77
- </svg>
78
- <div class="govuk-footer__meta">
79
- <div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
80
- <h2 class="govuk-visually-hidden">Support links</h2>
81
- {{$footerSupportLinks}}{{/footerSupportLinks}}
82
- <svg
83
- aria-hidden="true"
84
- focusable="false"
85
- class="govuk-footer__licence-logo"
86
- xmlns="http://www.w3.org/2000/svg"
87
- viewBox="0 0 483.2 195.7"
88
- height="17"
89
- width="41">
90
- <path
91
- fill="currentColor"
92
- d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145" />
93
- </svg>
94
- <span class="govuk-footer__licence-description">
95
- {{$licenceMessage}}All content is available under the <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" id="open-government-licence" class="govuk-footer__link" target="_blank" rel="license">Open Government Licence v3.0</a>, except where otherwise stated{{/licenceMessage}}
96
- </span>
97
- </div>
98
- <div class="govuk-footer__meta-item">
99
- <a
100
- class="govuk-footer__link govuk-footer__copyright-logo"
101
- href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">
102
- {{$crownCopyrightMessage}}© Crown copyright{{/crownCopyrightMessage}}
103
- </a>
104
- </div>
105
- </div>
106
- </div>
107
- </footer>
108
-
109
- <div id="global-app-error" class="app-error hidden"></div>
110
-
111
-
112
- {{$bodyEnd}}{{/bodyEnd}}
113
-
114
-
115
- <script {{#nonce}}nonce="{{nonce}}"{{/nonce}}>if (typeof window.GOVUK === 'undefined') document.body.className = document.body.className.replace('js-enabled', '');</script>
116
-
117
- </body>
118
- </html>