From a69837029eb92f5e41339751427e972c84daf8a9 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 19 Nov 2025 12:12:51 +0000 Subject: [PATCH 01/11] Stash --- src/config/index.ts | 6 ------ src/server/plugins/nunjucks/context.js | 6 +----- src/server/plugins/nunjucks/context.test.js | 3 --- src/server/views/confirmation.html | 3 +++ src/server/views/layout.html | 9 ++------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 40717c91f..a84737a81 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -93,12 +93,6 @@ export const config = convict({ default: null, env: 'SERVICE_VERSION' } as SchemaObj, - feedbackLink: { - doc: 'Used in your phase banner. Can be a URL or more commonly mailto mailto:feedback@department.gov.uk', - format: String, - default: null, - env: 'FEEDBACK_LINK' - } as SchemaObj, phaseTag: { format: String, default: 'beta', // Accepts "alpha" |"beta" | "" diff --git a/src/server/plugins/nunjucks/context.js b/src/server/plugins/nunjucks/context.js index c748c6bc6..529fa01eb 100644 --- a/src/server/plugins/nunjucks/context.js +++ b/src/server/plugins/nunjucks/context.js @@ -1,10 +1,7 @@ import { readFileSync } from 'node:fs' import { basename, join } from 'node:path' -import { - checkFormStatus, - encodeUrl -} from '@defra/forms-engine-plugin/engine/helpers.js' +import { checkFormStatus } from '@defra/forms-engine-plugin/engine/helpers.js' import Boom from '@hapi/boom' import { StatusCodes } from 'http-status-codes' @@ -52,7 +49,6 @@ export function context(request) { config: { cdpEnvironment: config.get('cdpEnvironment'), designerUrl: config.get('designerUrl'), - feedbackLink: encodeUrl(config.get('feedbackLink')), phaseTag: config.get('phaseTag'), serviceBannerText: config.get('serviceBannerText'), serviceName: config.get('serviceName'), diff --git a/src/server/plugins/nunjucks/context.test.js b/src/server/plugins/nunjucks/context.test.js index bcd8870ce..b1d6c1ee6 100644 --- a/src/server/plugins/nunjucks/context.test.js +++ b/src/server/plugins/nunjucks/context.test.js @@ -1,7 +1,5 @@ import { tmpdir } from 'node:os' -import { encodeUrl } from '@defra/forms-engine-plugin/engine/helpers.js' - import { config } from '~/src/config/index.js' import { context } from '~/src/server/plugins/nunjucks/context.js' @@ -63,7 +61,6 @@ describe('Nunjucks context', () => { expect(ctx.config).toEqual( expect.objectContaining({ cdpEnvironment: config.get('cdpEnvironment'), - feedbackLink: encodeUrl(config.get('feedbackLink')), googleAnalyticsTrackingId: config.get('googleAnalyticsTrackingId'), phaseTag: config.get('phaseTag'), serviceBannerText: config.get('serviceBannerText'), diff --git a/src/server/views/confirmation.html b/src/server/views/confirmation.html index e2975622e..6031cef4a 100644 --- a/src/server/views/confirmation.html +++ b/src/server/views/confirmation.html @@ -14,6 +14,9 @@

What happens next

{{ submissionGuidance | markdown | safe }}
+

+ What do you think of this service? (takes 30 seconds) +

{% endblock %} diff --git a/src/server/views/layout.html b/src/server/views/layout.html index 28e3ef758..d8bb703b8 100644 --- a/src/server/views/layout.html +++ b/src/server/views/layout.html @@ -131,17 +131,12 @@ }) }} {% endblock %} {% block beforeContent %} - {% set feedbackLink = feedbackLink or config.feedbackLink -%} {% set phaseTag = phaseTag or config.phaseTag -%} - + {% set feedbackLink = '/form/csat?src=' + slug %} {% if phaseTag %} {% set feedbackLinkHtml -%} - {%- if "mailto:" in feedbackLink -%} - give your feedback by email - {%- else -%} - give your feedback (opens in new tab) - {%- endif -%} + give your feedback (opens in new tab) {%- endset %} From 58d13ddadcddca1a9a0e4b5fe69691877b78e67e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 24 Nov 2025 16:06:33 +0000 Subject: [PATCH 02/11] Stash --- src/server/index.ts | 7 +- src/server/plugins/FeedbackPageController.ts | 77 ++++++++++++++++++++ src/server/plugins/nunjucks/types.js | 1 + src/server/views/confirmation.html | 2 +- src/server/views/layout.html | 3 +- test/form/definitions/phase-alpha.json | 3 - 6 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/server/plugins/FeedbackPageController.ts diff --git a/src/server/index.ts b/src/server/index.ts index b6271ac68..b00bffb8c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -30,6 +30,7 @@ import { requestLogger } from '~/src/server/common/helpers/logging/request-logge import { requestTracing } from '~/src/server/common/helpers/logging/request-tracing.js' import { buildRedisClient } from '~/src/server/common/helpers/redis-client.js' import { FORM_PREFIX } from '~/src/server/constants.js' +import { FeedbackPageController } from '~/src/server/plugins/FeedbackPageController.js' import { SummaryPageWithConfirmationEmailController } from '~/src/server/plugins/SummaryPageWithConfirmationEmailController.js' import { configureBlankiePlugin } from '~/src/server/plugins/blankie.js' import { configureCrumbPlugin } from '~/src/server/plugins/crumb.js' @@ -133,7 +134,8 @@ export const configureEnginePlugin = async ({ model = new FormModel(definition, { basePath: initialBasePath }, services, { // Custom controllers - SummaryPageWithConfirmationEmailController + SummaryPageWithConfirmationEmailController, + FeedbackPageController }) } @@ -169,7 +171,8 @@ export const configureEnginePlugin = async ({ }, controllers: { // Custom controllers - SummaryPageWithConfirmationEmailController + SummaryPageWithConfirmationEmailController, + FeedbackPageController }, ordnanceSurveyApiKey: config.get('ordnanceSurveyApiKey') } diff --git a/src/server/plugins/FeedbackPageController.ts b/src/server/plugins/FeedbackPageController.ts new file mode 100644 index 000000000..b2ef313a8 --- /dev/null +++ b/src/server/plugins/FeedbackPageController.ts @@ -0,0 +1,77 @@ +import { type PageController } from '@defra/forms-engine-plugin/controllers/PageController.js' +import { QuestionPageController } from '@defra/forms-engine-plugin/controllers/QuestionPageController.js' +import { SummaryPageController } from '@defra/forms-engine-plugin/controllers/SummaryPageController.js' +import { + type FormContext, + type FormContextRequest +} from '@defra/forms-engine-plugin/engine/types.js' +import { + type FormPageViewModel, + type FormRequestPayload, + type FormResponseToolkit +} from '@defra/forms-engine-plugin/types' + +export class FeedbackPageController extends QuestionPageController { + getViewModel( + request: FormContextRequest, + context: FormContext + ): FormPageViewModel { + const viewModel = super.getViewModel(request, context) + return { + ...viewModel, + allowSaveAndExit: false, + phaseTag: undefined, + submitButtonText: 'Send feedback', + name: context.state.formName + } + } + + getStatusPath() { + return '/feedback-status' + } + + /** + * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`. + * If a form is incomplete, a user will be redirected to the start page. + */ + makePostRouteHandler() { + return async ( + request: FormRequestPayload, + context: FormContext, + h: FormResponseToolkit + ) => { + // Should not have to coerce the type - ticket to resolve later https://eaflood.atlassian.net/browse/DF-555 + const { viewName, model } = this as unknown as PageController + const { collection } = this + const { isForceAccess, state, evaluationState } = context + + /** + * If there are any errors, render the page with the parsed errors + * @todo Refactor to match POST REDIRECT GET pattern + */ + if (context.errors || isForceAccess) { + const viewModel = this.getViewModel(request, context) + viewModel.errors = collection.getViewErrors(viewModel.errors) + + // Filter our components based on their conditions using our evaluated state + viewModel.components = this.filterConditionalComponents( + viewModel, + model, + evaluationState + ) + + return h.view(viewName, viewModel) + } + + // Save state + await this.setState(request, state) + + const summary = new SummaryPageController( + model, + context.pageMap.get(context.paths[0]) + ) + // Should not have to coerce the type - ticket to resolve later https://eaflood.atlassian.net/browse/DF-555 + return summary.handleFormSubmit(request, context, h) + } + } +} diff --git a/src/server/plugins/nunjucks/types.js b/src/server/plugins/nunjucks/types.js index c82db7eef..f72a977f8 100644 --- a/src/server/plugins/nunjucks/types.js +++ b/src/server/plugins/nunjucks/types.js @@ -20,6 +20,7 @@ * @property {string} [currentPath] - Current path * @property {string} [previewMode] - Preview mode * @property {string} [slug] - Form slug + * @property {string} [formId] - Form id * @property {(asset?: string) => string} getAssetPath - Asset path resolver * @property {FormContext} [context] - the current form context */ diff --git a/src/server/views/confirmation.html b/src/server/views/confirmation.html index 6031cef4a..fd20d33a4 100644 --- a/src/server/views/confirmation.html +++ b/src/server/views/confirmation.html @@ -15,7 +15,7 @@

What happens next

{{ submissionGuidance | markdown | safe }}

- What do you think of this service? (takes 30 seconds) + What do you think of this service? (takes 30 seconds)

diff --git a/src/server/views/layout.html b/src/server/views/layout.html index d8bb703b8..a968f9a76 100644 --- a/src/server/views/layout.html +++ b/src/server/views/layout.html @@ -131,8 +131,7 @@ }) }} {% endblock %} {% block beforeContent %} - {% set phaseTag = phaseTag or config.phaseTag -%} - {% set feedbackLink = '/form/csat?src=' + slug %} + {% set feedbackLink = feedbackLink or config.feedbackLink %} {% if phaseTag %} {% set feedbackLinkHtml -%} diff --git a/test/form/definitions/phase-alpha.json b/test/form/definitions/phase-alpha.json index 5cd1ee4c5..c7b54bf5c 100644 --- a/test/form/definitions/phase-alpha.json +++ b/test/form/definitions/phase-alpha.json @@ -24,9 +24,6 @@ "sections": [], "conditions": [], "name": "Alpha form", - "feedback": { - "feedbackForm": false - }, "phaseBanner": { "phase": "alpha" } From 8df4dc8d05756caeb4a74b40a49e026aa4bb861b Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Tue, 25 Nov 2025 14:58:57 +0000 Subject: [PATCH 03/11] CSAT feedback changes --- src/server/plugins/FeedbackPageController.ts | 5 ---- src/server/views/confirmation.html | 29 +++++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/server/plugins/FeedbackPageController.ts b/src/server/plugins/FeedbackPageController.ts index b2ef313a8..d0e16574f 100644 --- a/src/server/plugins/FeedbackPageController.ts +++ b/src/server/plugins/FeedbackPageController.ts @@ -26,10 +26,6 @@ export class FeedbackPageController extends QuestionPageController { } } - getStatusPath() { - return '/feedback-status' - } - /** * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`. * If a form is incomplete, a user will be redirected to the start page. @@ -70,7 +66,6 @@ export class FeedbackPageController extends QuestionPageController { model, context.pageMap.get(context.paths[0]) ) - // Should not have to coerce the type - ticket to resolve later https://eaflood.atlassian.net/browse/DF-555 return summary.handleFormSubmit(request, context, h) } } diff --git a/src/server/views/confirmation.html b/src/server/views/confirmation.html index fd20d33a4..f9459b195 100644 --- a/src/server/views/confirmation.html +++ b/src/server/views/confirmation.html @@ -4,19 +4,40 @@ {% set mainClasses = "govuk-main-wrapper--l" %} +{% set subTitle = "What happens next" %} + +{% set controllers = "" %} +{% for page in page.model.def.pages %} + {% set controllers = controllers + page.controller + ", " %} +{% endfor %} + +{# Override values if this is a submission from a feedback form i.e. it uses FeedbacpPageController #} +{% if "FeedbackPageController," in controllers %} + {% set pageTitle = "Feedback submitted"%} + {% set submissionGuidance = "Thank you for leaving feedback.\n\nYou can close this screen now."%} + {% set subTitle = undefined %} + {% set feedbackLink = undefined %} + {% set name = formName if formName else name %} + {% set phaseTag = undefined %} +{% endif %} + {% block content %} {% endblock %} From 2829bc5b17d3307f08f32d2f9df52a38e6b43c8b Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Tue, 25 Nov 2025 17:07:51 +0000 Subject: [PATCH 04/11] Updated engine version --- package-lock.json | 60 +++++++++++++++++++++++++++++------------------ package.json | 3 ++- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c301b426..dfb97313e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.934.0", - "@defra/forms-engine-plugin": "^4.0.19", + "@defra/forms-engine-plugin": "^4.0.25", "@defra/forms-model": "^3.0.581", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -34,6 +34,7 @@ "blankie": "^5.0.0", "blipp": "^4.0.2", "btoa": "^1.2.1", + "chokidar": "^3.6.0", "convict": "^6.2.4", "date-fns": "^4.1.0", "dotenv": "^17.2.3", @@ -860,6 +861,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2716,6 +2718,7 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2829,6 +2832,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2852,6 +2856,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2904,13 +2909,13 @@ } }, "node_modules/@defra/forms-engine-plugin": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.0.19.tgz", - "integrity": "sha512-Aem8zH4DGp3elXf1qD9NICjiG2yOB7Mj2Yv+hYD580Ov4vKYNWVppRjJP4RS3izihquhz3QqhiQL+YuMJp2LdA==", + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.0.25.tgz", + "integrity": "sha512-rbhtOmNodIlkGxINFGOpPkqSQyUjpV3KbUktrWmyIy4l43PEK1HZa3Q2Tz+CxHBjyu25VrZjS1IR93rgWzwSWw==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.580", + "@defra/forms-model": "^3.0.584", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", @@ -2931,6 +2936,7 @@ "blankie": "^5.0.0", "blipp": "^4.0.2", "btoa": "^1.2.1", + "chokidar": "3.6.0", "convict": "^6.2.4", "date-fns": "^4.1.0", "dotenv": "^17.2.3", @@ -2972,9 +2978,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.581", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.581.tgz", - "integrity": "sha512-bGzwOGIelVXEspNCTBm/g2pi+Qu70d1Q/r7lmYK9XmUEO9/TIOWISMXl2um87M5ICC6xCrFiGdhqgKuvn6aO0Q==", + "version": "3.0.585", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.585.tgz", + "integrity": "sha512-lSJzQu0xTk+VqSjLcjt7zwPS88J7MVYl5kdKfKCQsqbnVTmBHi9a3MHvCa6xlZRu1GZgoEjwQWY+QKzTKfN8FA==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", @@ -6604,6 +6610,7 @@ "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.47.0", @@ -6634,6 +6641,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -7418,6 +7426,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7573,7 +7582,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -7884,6 +7892,7 @@ "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/sinon": "^17.0.3", "sinon": "^18.0.1", @@ -8200,7 +8209,6 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" }, @@ -8272,7 +8280,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -8301,6 +8308,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -8543,7 +8551,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", - "optional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -10058,6 +10065,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10308,6 +10316,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10481,6 +10490,7 @@ "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", @@ -10544,6 +10554,7 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10916,7 +10927,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11079,7 +11089,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11305,7 +11314,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12021,7 +12029,6 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", - "optional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -12139,7 +12146,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12211,7 +12217,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -12250,7 +12255,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -12627,6 +12631,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -13714,6 +13719,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -13723,6 +13729,7 @@ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -13791,6 +13798,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -14566,7 +14574,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15093,7 +15100,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -15409,6 +15415,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15972,6 +15979,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16248,7 +16256,6 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", - "optional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -17221,6 +17228,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18015,6 +18023,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -18750,6 +18759,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -18810,7 +18820,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -18916,6 +18925,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -19050,6 +19060,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19135,6 +19146,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -19335,6 +19347,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19403,6 +19416,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", diff --git a/package.json b/package.json index b6f5b7459..e28bf8bd1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.934.0", - "@defra/forms-engine-plugin": "^4.0.19", + "@defra/forms-engine-plugin": "^4.0.25", "@defra/forms-model": "^3.0.581", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -64,6 +64,7 @@ "blankie": "^5.0.0", "blipp": "^4.0.2", "btoa": "^1.2.1", + "chokidar": "^3.6.0", "convict": "^6.2.4", "date-fns": "^4.1.0", "dotenv": "^17.2.3", From 1d14bd228c22490c9d13076ee195eef04b43c8a3 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 26 Nov 2025 12:01:02 +0000 Subject: [PATCH 05/11] Tidy up and fixed tests --- src/server/plugins/FeedbackPageController.ts | 2 +- .../error-preview/error-preview.test.js | 6 ++-- src/server/plugins/router.ts | 29 +++++++++++++++++-- src/server/views/confirmation.html | 6 ++-- src/server/views/layout.html | 4 ++- test/form/feedback.test.js | 23 ++++----------- 6 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/server/plugins/FeedbackPageController.ts b/src/server/plugins/FeedbackPageController.ts index d0e16574f..51fdbdbe7 100644 --- a/src/server/plugins/FeedbackPageController.ts +++ b/src/server/plugins/FeedbackPageController.ts @@ -20,7 +20,7 @@ export class FeedbackPageController extends QuestionPageController { return { ...viewModel, allowSaveAndExit: false, - phaseTag: undefined, + hidePhaseBanner: true, submitButtonText: 'Send feedback', name: context.state.formName } diff --git a/src/server/plugins/error-preview/error-preview.test.js b/src/server/plugins/error-preview/error-preview.test.js index 5312ce78c..c28283d9b 100644 --- a/src/server/plugins/error-preview/error-preview.test.js +++ b/src/server/plugins/error-preview/error-preview.test.js @@ -47,17 +47,17 @@ describe('Error preview route', () => { 'govuk-error-summary__title govuk-!-margin-bottom-2' ) - expect($links[4].textContent).toBe('Enter [short description]') + expect($links[3].textContent).toBe('Enter [short description]') expect($headings[3].textContent.trim()).toBe('If you set answer limits') expect($headings[3]).toHaveClass( 'govuk-error-summary__title govuk-!-margin-bottom-2' ) - expect($links[5].textContent).toBe( + expect($links[4].textContent).toBe( '[short description] must be [min length] characters or more' ) - expect($links[6].textContent).toBe( + expect($links[5].textContent).toBe( '[short description] must be [max length] characters or less' ) }) diff --git a/src/server/plugins/router.ts b/src/server/plugins/router.ts index 2713525f7..9f66cf121 100644 --- a/src/server/plugins/router.ts +++ b/src/server/plugins/router.ts @@ -8,6 +8,7 @@ import { pathSchema, stateSchema } from '@defra/forms-engine-plugin/schema.js' +import { FormStatus } from '@defra/forms-engine-plugin/types' import { slugSchema } from '@defra/forms-model' import Boom from '@hapi/boom' import { @@ -33,7 +34,10 @@ import { publicRoutes, saveAndExitRoutes } from '~/src/server/routes/index.js' -import { getFormMetadata } from '~/src/server/services/formsService.js' +import { + getFormDefinition, + getFormMetadata +} from '~/src/server/services/formsService.js' const routes: ServerRoute[] = [...publicRoutes, healthRoute] const saveAndExitExpiryDays = config.get('saveAndExitExpiryDays') @@ -146,16 +150,35 @@ export default { server.route<{ Params: { slug: string } }>({ method: 'get', path: '/help/cookies/{slug}', - handler(_request, h) { + async handler(request, h) { const sessionTimeout = config.get('sessionTimeout') const sessionDurationPretty = humanizeDuration(sessionTimeout) + const { slug } = request.params + + let formId = '' + let formTitle + + if (slug) { + try { + const meta = await getFormMetadata(request.params.slug) + const definition = await getFormDefinition( + meta.id, + FormStatus.Draft + ) + formId = meta.id + formTitle = definition?.name + } catch {} + } + return h.view('help/cookies', { googleAnalyticsContainerId: config .get('googleAnalyticsTrackingId') .replace(/^G-/, ''), - sessionDurationPretty + sessionDurationPretty, + name: formTitle, + feedbackLink: `/form/csat?formId=${formId}` }) }, options diff --git a/src/server/views/confirmation.html b/src/server/views/confirmation.html index d3ca45051..4d2b9a6b9 100644 --- a/src/server/views/confirmation.html +++ b/src/server/views/confirmation.html @@ -11,14 +11,16 @@ {% set controllers = controllers + page.controller + ", " %} {% endfor %} +{% set phaseTag = phaseTag or config.phaseTag %} + {# Override values if this is a submission from a feedback form i.e. it uses FeedbacpPageController #} {% if "FeedbackPageController," in controllers %} {% set pageTitle = "Feedback submitted"%} {% set submissionGuidance = "Thank you for leaving feedback.\n\nYou can close this screen now."%} {% set subTitle = undefined %} {% set feedbackLink = undefined %} - {% set name = formName if formName else name %} - {% set phaseTag = undefined %} + {% set name = formName if formName else "Submit a form to Defra" %} + {% set hidePhaseBanner = true %} {% endif %} {% block content %} diff --git a/src/server/views/layout.html b/src/server/views/layout.html index a968f9a76..216b52979 100644 --- a/src/server/views/layout.html +++ b/src/server/views/layout.html @@ -45,6 +45,8 @@ }) }} {% endblock %} +{% set phaseTag = phaseTag or config.phaseTag %} + {% block header %} {% if config.googleAnalyticsTrackingId and slug %}
@@ -132,7 +134,7 @@ {% endblock %} {% block beforeContent %} {% set feedbackLink = feedbackLink or config.feedbackLink %} - {% if phaseTag %} + {% if phaseTag and not hidePhaseBanner %} {% set feedbackLinkHtml -%} give your feedback (opens in new tab) diff --git a/test/form/feedback.test.js b/test/form/feedback.test.js index b451ad0eb..15f8b59af 100644 --- a/test/form/feedback.test.js +++ b/test/form/feedback.test.js @@ -1,14 +1,10 @@ import { join } from 'node:path' -import { FORM_PREFIX } from '~/src/server/constants.js' import { createServer } from '~/src/server/index.js' import { getFormMetadata } from '~/src/server/services/formsService.js' import * as fixtures from '~/test/fixtures/index.js' import { renderResponse } from '~/test/helpers/component-helpers.js' -const { FEEDBACK_LINK } = process.env -const basePath = `${FORM_PREFIX}/feedback` - jest.mock('~/src/server/services/formsService.js') describe('Feedback link', () => { @@ -33,24 +29,15 @@ describe('Feedback link', () => { await server.stop() }) - it.each([ - { - url: `${FORM_PREFIX}/help/cookies`, - name: 'give your feedback (opens in new tab)', - href: FEEDBACK_LINK - }, - { - // Email address from feedback.json - url: `${basePath}/uk-passport`, - name: 'give your feedback by email', - href: 'mailto:test@feedback.cat' - } - ])("should match route '$url'", async ({ url, name, href }) => { + it('should match route', async () => { const { container } = await renderResponse(server, { method: 'GET', - url + url: '/help/cookies/feedback' }) + const name = 'give your feedback (opens in new tab)' + const href = '/form/csat?formId=661e4ca5039739ef2902b214' + const $phaseBanner = document.querySelector('.govuk-phase-banner') const $link = container.getByRole('link', { name }) From 03385f8c09f902dbe3475ac52b0be8ea34e4026d Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 26 Nov 2025 13:21:52 +0000 Subject: [PATCH 06/11] Extra coverage --- .../plugins/FeedbackPageController.test.ts | 96 ++++++++++++++++ test/form/definitions/csat.js | 106 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/server/plugins/FeedbackPageController.test.ts create mode 100644 test/form/definitions/csat.js diff --git a/src/server/plugins/FeedbackPageController.test.ts b/src/server/plugins/FeedbackPageController.test.ts new file mode 100644 index 000000000..570911aee --- /dev/null +++ b/src/server/plugins/FeedbackPageController.test.ts @@ -0,0 +1,96 @@ +import { type QuestionPageController } from '@defra/forms-engine-plugin/controllers/QuestionPageController.js' +import { FormModel } from '@defra/forms-engine-plugin/engine/models/FormModel.js' +import { buildFormRequest } from '@defra/forms-engine-plugin/engine/pageControllers/__stubs__/request.js' +import { type FormSubmissionState } from '@defra/forms-engine-plugin/engine/types.js' +import { + type FormContext, + type FormRequest, + type FormRequestPayload, + type FormResponseToolkit +} from '@defra/forms-engine-plugin/types' +import { type PageQuestion } from '@defra/forms-model' + +import { FeedbackPageController } from '~/src/server/plugins/FeedbackPageController.js' +import definition from '~/test/form/definitions/csat.js' + +describe('FeedbackPageController', () => { + let model: FormModel + let controller: FeedbackPageController + let requestPage: FormRequest + + const response = { + code: jest.fn().mockImplementation(() => response) + } + const h: FormResponseToolkit = { + redirect: jest.fn().mockReturnValue(response), + view: jest.fn() + } + + beforeEach(() => { + model = new FormModel(definition, { + basePath: 'test' + }) + + // Create a mock page for FeedbackPageController + const mockPage = { + ...definition.pages[0], + controller: 'FeedbackPageController' + } as unknown as PageQuestion + + controller = new FeedbackPageController(model, mockPage) + + requestPage = buildFormRequest({ + method: 'get', + url: new URL('http://example.com/test/give-feedback'), + path: '/test/give-feedback', + params: { + path: 'give-feedback', + slug: 'test' + }, + query: {}, + app: { model } + } as FormRequest) + }) + + describe('handle errors', () => { + it('should display errors including summary', async () => { + const state: FormSubmissionState = { + $$__referenceNumber: 'foobar', + formId: 365 + } + const request = { + ...requestPage, + method: 'post', + payload: { invalid: '123', action: 'send' } + } as unknown as FormRequestPayload + + // Add error + const context = { + ...model.getFormContext(request, state), + errors: [ + { + name: 'PMPyjg', + path: '/feedback', + text: 'Select how you feel about this service' + } + ] + } as FormContext + + jest + .spyOn(controller as unknown as QuestionPageController, 'getState') + .mockResolvedValue({}) + jest + .spyOn(controller as unknown as QuestionPageController, 'setState') + .mockResolvedValue(state) + + const postHandler = controller.makePostRouteHandler() + await postHandler(request, context, h) + + const viewModel = controller.getViewModel(request, context) + + expect(viewModel.errors).toHaveLength(1) + const errorText = viewModel.errors ? viewModel.errors[0].text : '' + expect(errorText).toBe('Select how you feel about this service') + }) + }) +}) diff --git a/test/form/definitions/csat.js b/test/form/definitions/csat.js new file mode 100644 index 000000000..9925de5f1 --- /dev/null +++ b/test/form/definitions/csat.js @@ -0,0 +1,106 @@ +import { + ComponentType, + ControllerType, + Engine, + SchemaVersion +} from '@defra/forms-model' + +export default /** @satisfies {FormDefinition} */ ({ + name: 'CSAT', + engine: Engine.V2, + schema: SchemaVersion.V2, + startPage: '/give-feedback', + pages: [ + { + title: 'Give feedback', + path: '/give-feedback', + components: [ + { + type: ComponentType.RadiosField, + title: + 'Overall, how do you feel about the service you received today?', + name: 'PMPyjg', + shortDescription: 'How you feel about the service', + hint: '', + options: { + required: true + }, + list: '7acae132-d54d-4663-929d-04a1ed4d35d3', + id: 'b5c525db-068e-4f05-818a-7ef57303f8b5' + }, + { + type: ComponentType.MultilineTextField, + title: 'How could we improve this service?', + name: 'AiBocn', + shortDescription: 'How we could improve this service', + hint: '', + options: { + required: false + }, + schema: { + max: 1200 + }, + id: '57c2b549-e152-469f-9250-865ce5a4de23' + }, + { + type: ComponentType.HiddenField, + title: 'Hidden form id', + name: 'formId', + options: { + required: false + }, + id: '5ab73ef3-bce4-41a6-a8ae-9525160c36ed' + } + ], + next: [], + id: '4af5213f-0373-43b9-b32f-0ee822d37860' + }, + { + id: '449a45f6-4541-4a46-91bd-8b8931b07b50', + title: '', + path: '/summary', + controller: ControllerType.Summary + } + ], + conditions: [], + sections: [], + lists: [ + { + name: 'hgNvNj', + title: 'List for question PMPyjg', + type: 'string', + items: [ + { + id: '24c89938-25e6-4cfa-99c6-f84609dd1bd2', + text: 'Very satisfied', + value: 'Very satisfied' + }, + { + id: '631bcf2f-ba78-4627-9e1b-e9f1030dc0e0', + text: 'Satisfied', + value: 'Satisfied' + }, + { + id: 'ee4db6ec-7246-430e-8dd2-e24cf4c34376', + text: 'Neither satisfied not dissatisfied', + value: 'Neither satisfied not dissatisfied' + }, + { + id: '3b720c19-e580-405d-81ca-189452e9d873', + text: 'Dissatisfied', + value: 'Dissatisfied' + }, + { + id: '4f4c7e11-f27a-470d-a366-48dd9a25f24c', + text: 'Very dissatisfied', + value: 'Very dissatisfied' + } + ], + id: '7acae132-d54d-4663-929d-04a1ed4d35d3' + } + ] +}) + +/** + * @import { FormDefinition } from '@defra/forms-model' + */ From de01201b99d093bbe858bcc28bc72be95470bfec Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 27 Nov 2025 09:40:28 +0000 Subject: [PATCH 07/11] Added CSAT journey test --- test/form/csat.test.js | 212 ++++++++++++++++++ .../csat-with-custom-controller.js | 97 ++++++++ 2 files changed, 309 insertions(+) create mode 100644 test/form/csat.test.js create mode 100644 test/form/definitions/csat-with-custom-controller.js diff --git a/test/form/csat.test.js b/test/form/csat.test.js new file mode 100644 index 000000000..7fe7d4e19 --- /dev/null +++ b/test/form/csat.test.js @@ -0,0 +1,212 @@ +import { join } from 'node:path' + +import { SummaryPageController } from '@defra/forms-engine-plugin/controllers/SummaryPageController.js' +import { getCacheService } from '@defra/forms-engine-plugin/engine/helpers.js' +import { within } from '@testing-library/dom' +import { StatusCodes } from 'http-status-codes' + +import { FORM_PREFIX } from '~/src/server/constants.js' +import { createServer } from '~/src/server/index.js' +import { + getFormMetadata, + getFormMetadataById +} from '~/src/server/services/formsService.js' +import * as fixtures from '~/test/fixtures/index.js' +import { renderResponse } from '~/test/helpers/component-helpers.js' +import { getCookie, getCookieHeader } from '~/test/utils/get-cookie.js' + +jest.mock('~/src/server/services/formsService.js') +jest.mock('~/src/server/messaging/publish.js') +jest.mock('@defra/forms-engine-plugin/services/formSubmissionService.js') +jest.mock('@defra/forms-engine-plugin/controllers/SummaryPageController.js') + +const basePath = `${FORM_PREFIX}/csat-with-custom-controller` + +const metadata = { + ...fixtures.form.metadata, + notificationEmail: undefined +} + +describe('CSAT journey', () => { + const journey = [ + /** + * Question page 1 + */ + { + heading1: 'Give feedback', + + paths: { + current: '/give-feedback', + next: '/status' + }, + + fields: [ + { + title: + 'Overall, how do you feel about the service you received today?', + text: 'How do you feel about the service', + payload: { + empty: { PMPyjg: '', AiBocn: '', formId: '' }, + valid: { + PMPyjg: 'Very satisfied', + AiBocn: 'some extra text', + formId: '123' + } + }, + + errors: { + empty: 'Select how you feel about the service' + } + } + ] + }, + + /** + * Submitted + */ + { + heading1: 'Feedback submitted', + + paths: { + current: '/status', + previous: '/give-feedback' + } + } + ] + + /** @type {Server} */ + let server + + /** @type {string} */ + let csrfToken + + /** @type {ReturnType} */ + let headers + + /** @type {BoundFunctions} */ + let container + + // Create server before each test + beforeAll(async () => { + server = await createServer({ + formFileName: 'csat-with-custom-controller.js', + formFilePath: join(import.meta.dirname, 'definitions'), + enforceCsrf: true + }) + + await server.initialize() + + // Navigate to start + const response = await server.inject({ + url: `${basePath}${journey[0].paths.current}` + }) + + // Extract the session cookie + csrfToken = getCookie(response, 'crumb') + headers = getCookieHeader(response, ['session', 'crumb']) + }) + + beforeEach(() => { + jest.clearAllMocks() + jest.mocked(getFormMetadata).mockResolvedValue(metadata) + jest.mocked(getFormMetadataById).mockResolvedValue(metadata) + jest + .spyOn(SummaryPageController.prototype, 'handleFormSubmit') + .mockImplementation((request, _context, h) => { + const cacheService = getCacheService(request.server) + // Should be able to void this but linter still doesnt like it + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dummy = Promise.resolve( + cacheService.setConfirmationState(request, { + confirmed: true, + formId: 'dummyId' + }) + ) + return Promise.resolve(h.redirect(`${basePath}/status`)) + }) + }) + + afterAll(async () => { + await server.stop() + }) + + describe.each(journey)( + 'Page: $paths.current', + ({ heading1, paths, fields = [] }) => { + beforeEach(async () => { + ;({ container } = await renderResponse(server, { + url: `${basePath}${paths.current}`, + headers + })) + }) + + it('should render the page heading', () => { + const $heading = container.getByRole('heading', { + name: heading1, + level: 1 + }) + + expect($heading).toBeInTheDocument() + }) + + if (paths.next) { + it('should show errors when invalid on submit', async () => { + const payload = {} + + for (const field of fields) { + Object.assign(payload, field.payload.empty) + } + + // Submit form with empty values + const { container, response } = await renderResponse(server, { + url: `${basePath}${paths.current}`, + method: 'POST', + headers, + payload: { ...payload, crumb: csrfToken } + }) + + expect(response.statusCode).toBe(StatusCodes.OK) + expect(response.headers.location).toBeUndefined() + + const $errorSummary = container.getByRole('alert') + const $errorItems = within($errorSummary).getAllByRole('listitem') + + const $heading = within($errorSummary).getByRole('heading', { + name: 'There is a problem', + level: 2 + }) + + expect($heading).toBeInTheDocument() + + for (const [index, { errors }] of fields.entries()) { + expect($errorItems[index]).toHaveTextContent(errors.empty) + } + }) + + it('should redirect to the next page on submit', async () => { + const payload = {} + + for (const field of fields) { + Object.assign(payload, field.payload.valid) + } + + // Submit form with populated values + const response = await server.inject({ + url: `${basePath}${paths.current}`, + method: 'POST', + headers, + payload: { ...payload, crumb: csrfToken } + }) + + expect(response.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) + expect(response.headers.location).toBe(`${basePath}${paths.next}`) + }) + } + } + ) +}) + +/** + * @import { Server } from '@hapi/hapi' + * @import { BoundFunctions, queries } from '@testing-library/dom' + */ diff --git a/test/form/definitions/csat-with-custom-controller.js b/test/form/definitions/csat-with-custom-controller.js new file mode 100644 index 000000000..4e25ba79a --- /dev/null +++ b/test/form/definitions/csat-with-custom-controller.js @@ -0,0 +1,97 @@ +import { ComponentType, Engine, SchemaVersion } from '@defra/forms-model' + +export default /** @satisfies {FormDefinition} */ ({ + name: 'CSAT', + engine: Engine.V2, + schema: SchemaVersion.V2, + startPage: '/give-feedback', + pages: [ + { + title: 'Give feedback', + path: '/give-feedback', + components: [ + { + type: ComponentType.RadiosField, + title: + 'Overall, how do you feel about the service you received today?', + name: 'PMPyjg', + shortDescription: 'How you feel about the service', + hint: '', + options: { + required: true + }, + list: '7acae132-d54d-4663-929d-04a1ed4d35d3', + id: 'b5c525db-068e-4f05-818a-7ef57303f8b5' + }, + { + type: ComponentType.MultilineTextField, + title: 'How could we improve this service?', + name: 'AiBocn', + shortDescription: 'How we could improve this service', + hint: '', + options: { + required: false + }, + schema: { + max: 1200 + }, + id: '57c2b549-e152-469f-9250-865ce5a4de23' + }, + { + type: ComponentType.HiddenField, + title: 'Hidden form id', + name: 'formId', + options: { + required: false + }, + id: '5ab73ef3-bce4-41a6-a8ae-9525160c36ed' + } + ], + next: [], + id: '4af5213f-0373-43b9-b32f-0ee822d37860', + // @ts-expect-error - custom controller name + controller: 'FeedbackPageController' + } + ], + conditions: [], + sections: [], + lists: [ + { + name: 'hgNvNj', + title: 'List for question PMPyjg', + type: 'string', + items: [ + { + id: '24c89938-25e6-4cfa-99c6-f84609dd1bd2', + text: 'Very satisfied', + value: 'Very satisfied' + }, + { + id: '631bcf2f-ba78-4627-9e1b-e9f1030dc0e0', + text: 'Satisfied', + value: 'Satisfied' + }, + { + id: 'ee4db6ec-7246-430e-8dd2-e24cf4c34376', + text: 'Neither satisfied not dissatisfied', + value: 'Neither satisfied not dissatisfied' + }, + { + id: '3b720c19-e580-405d-81ca-189452e9d873', + text: 'Dissatisfied', + value: 'Dissatisfied' + }, + { + id: '4f4c7e11-f27a-470d-a366-48dd9a25f24c', + text: 'Very dissatisfied', + value: 'Very dissatisfied' + } + ], + id: '7acae132-d54d-4663-929d-04a1ed4d35d3' + } + ] +}) + +/** + * @import { FormDefinition } from '@defra/forms-model' + */ From aff6b5e994194d3a0ec959acaf477e560ab6b1d0 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 27 Nov 2025 16:39:42 +0000 Subject: [PATCH 08/11] Updated plugin version Added journey tests --- package-lock.json | 10 +++++----- package.json | 2 +- src/server/plugins/FeedbackPageController.test.ts | 2 +- src/server/plugins/router.ts | 2 +- ...ller.js => user-feedback-with-custom-controller.js} | 2 +- test/form/definitions/{csat.js => user-feedback.js} | 2 +- test/form/feedback.test.js | 2 +- test/form/{csat.test.js => user-feedback.test.js} | 6 +++--- 8 files changed, 14 insertions(+), 14 deletions(-) rename test/form/definitions/{csat-with-custom-controller.js => user-feedback-with-custom-controller.js} (99%) rename test/form/definitions/{csat.js => user-feedback.js} (99%) rename test/form/{csat.test.js => user-feedback.test.js} (97%) diff --git a/package-lock.json b/package-lock.json index 30e372a5d..7fd1bc8a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.934.0", - "@defra/forms-engine-plugin": "^4.0.25", + "@defra/forms-engine-plugin": "^4.0.26", "@defra/forms-model": "^3.0.585", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -2909,13 +2909,13 @@ } }, "node_modules/@defra/forms-engine-plugin": { - "version": "4.0.25", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.0.25.tgz", - "integrity": "sha512-rbhtOmNodIlkGxINFGOpPkqSQyUjpV3KbUktrWmyIy4l43PEK1HZa3Q2Tz+CxHBjyu25VrZjS1IR93rgWzwSWw==", + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.0.26.tgz", + "integrity": "sha512-D8Q5dPKleQB3q4Xh5+CPk//fcA6Ul9NplRo+4l5uGIbhSAsrJiQc5nqUA6aUz6Lj0QYjIhIhK2Tj2B5pluMtDA==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.584", + "@defra/forms-model": "^3.0.585", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", diff --git a/package.json b/package.json index ea809d866..9a0be2a68 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.934.0", - "@defra/forms-engine-plugin": "^4.0.25", + "@defra/forms-engine-plugin": "^4.0.26", "@defra/forms-model": "^3.0.585", "@defra/hapi-tracing": "^1.29.0", "@elastic/ecs-pino-format": "^1.5.0", diff --git a/src/server/plugins/FeedbackPageController.test.ts b/src/server/plugins/FeedbackPageController.test.ts index 570911aee..03c259dcf 100644 --- a/src/server/plugins/FeedbackPageController.test.ts +++ b/src/server/plugins/FeedbackPageController.test.ts @@ -11,7 +11,7 @@ import { import { type PageQuestion } from '@defra/forms-model' import { FeedbackPageController } from '~/src/server/plugins/FeedbackPageController.js' -import definition from '~/test/form/definitions/csat.js' +import definition from '~/test/form/definitions/user-feedback.js' describe('FeedbackPageController', () => { let model: FormModel diff --git a/src/server/plugins/router.ts b/src/server/plugins/router.ts index 9f66cf121..c6eed1d90 100644 --- a/src/server/plugins/router.ts +++ b/src/server/plugins/router.ts @@ -178,7 +178,7 @@ export default { .replace(/^G-/, ''), sessionDurationPretty, name: formTitle, - feedbackLink: `/form/csat?formId=${formId}` + feedbackLink: `/form/feedback?formId=${formId}` }) }, options diff --git a/test/form/definitions/csat-with-custom-controller.js b/test/form/definitions/user-feedback-with-custom-controller.js similarity index 99% rename from test/form/definitions/csat-with-custom-controller.js rename to test/form/definitions/user-feedback-with-custom-controller.js index 4e25ba79a..2e8a46732 100644 --- a/test/form/definitions/csat-with-custom-controller.js +++ b/test/form/definitions/user-feedback-with-custom-controller.js @@ -1,7 +1,7 @@ import { ComponentType, Engine, SchemaVersion } from '@defra/forms-model' export default /** @satisfies {FormDefinition} */ ({ - name: 'CSAT', + name: 'User feedback', engine: Engine.V2, schema: SchemaVersion.V2, startPage: '/give-feedback', diff --git a/test/form/definitions/csat.js b/test/form/definitions/user-feedback.js similarity index 99% rename from test/form/definitions/csat.js rename to test/form/definitions/user-feedback.js index 9925de5f1..834d981a2 100644 --- a/test/form/definitions/csat.js +++ b/test/form/definitions/user-feedback.js @@ -6,7 +6,7 @@ import { } from '@defra/forms-model' export default /** @satisfies {FormDefinition} */ ({ - name: 'CSAT', + name: 'User feedback', engine: Engine.V2, schema: SchemaVersion.V2, startPage: '/give-feedback', diff --git a/test/form/feedback.test.js b/test/form/feedback.test.js index 15f8b59af..84c602fc3 100644 --- a/test/form/feedback.test.js +++ b/test/form/feedback.test.js @@ -36,7 +36,7 @@ describe('Feedback link', () => { }) const name = 'give your feedback (opens in new tab)' - const href = '/form/csat?formId=661e4ca5039739ef2902b214' + const href = '/form/feedback?formId=661e4ca5039739ef2902b214' const $phaseBanner = document.querySelector('.govuk-phase-banner') const $link = container.getByRole('link', { name }) diff --git a/test/form/csat.test.js b/test/form/user-feedback.test.js similarity index 97% rename from test/form/csat.test.js rename to test/form/user-feedback.test.js index 7fe7d4e19..f9aea668e 100644 --- a/test/form/csat.test.js +++ b/test/form/user-feedback.test.js @@ -20,14 +20,14 @@ jest.mock('~/src/server/messaging/publish.js') jest.mock('@defra/forms-engine-plugin/services/formSubmissionService.js') jest.mock('@defra/forms-engine-plugin/controllers/SummaryPageController.js') -const basePath = `${FORM_PREFIX}/csat-with-custom-controller` +const basePath = `${FORM_PREFIX}/user-feedback-with-custom-controller` const metadata = { ...fixtures.form.metadata, notificationEmail: undefined } -describe('CSAT journey', () => { +describe('User feedback journey', () => { const journey = [ /** * Question page 1 @@ -89,7 +89,7 @@ describe('CSAT journey', () => { // Create server before each test beforeAll(async () => { server = await createServer({ - formFileName: 'csat-with-custom-controller.js', + formFileName: 'user-feedback-with-custom-controller.js', formFilePath: join(import.meta.dirname, 'definitions'), enforceCsrf: true }) From 460f419c3c18f12a81d706a688d12b36d1e5f8bd Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 28 Nov 2025 09:07:38 +0000 Subject: [PATCH 09/11] Enhanced journey test --- src/server/views/confirmation.html | 2 +- test/form/user-feedback.test.js | 47 +++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/server/views/confirmation.html b/src/server/views/confirmation.html index 4d2b9a6b9..161070c4b 100644 --- a/src/server/views/confirmation.html +++ b/src/server/views/confirmation.html @@ -13,7 +13,7 @@ {% set phaseTag = phaseTag or config.phaseTag %} -{# Override values if this is a submission from a feedback form i.e. it uses FeedbacpPageController #} +{# Override values if this is a submission from a feedback form i.e. it uses FeedbackPageController #} {% if "FeedbackPageController," in controllers %} {% set pageTitle = "Feedback submitted"%} {% set submissionGuidance = "Thank you for leaving feedback.\n\nYou can close this screen now."%} diff --git a/test/form/user-feedback.test.js b/test/form/user-feedback.test.js index f9aea668e..c92281a8d 100644 --- a/test/form/user-feedback.test.js +++ b/test/form/user-feedback.test.js @@ -29,10 +29,23 @@ const metadata = { describe('User feedback journey', () => { const journey = [ + /** + * Pre-page + */ + { + // No title/name yet as URL saves param state and forwards to proper start page + + paths: { + current: '/give-feedback?formId=some-form-id' + } + }, + /** * Question page 1 */ { + formName: 'Test form', + heading1: 'Give feedback', paths: { @@ -65,6 +78,8 @@ describe('User feedback journey', () => { * Submitted */ { + formName: 'Test form', + heading1: 'Feedback submitted', paths: { @@ -88,6 +103,8 @@ describe('User feedback journey', () => { // Create server before each test beforeAll(async () => { + jest.mocked(getFormMetadataById).mockResolvedValue(metadata) + jest.mocked(getFormMetadata).mockResolvedValue(metadata) server = await createServer({ formFileName: 'user-feedback-with-custom-controller.js', formFilePath: join(import.meta.dirname, 'definitions'), @@ -132,7 +149,7 @@ describe('User feedback journey', () => { describe.each(journey)( 'Page: $paths.current', - ({ heading1, paths, fields = [] }) => { + ({ formName, heading1, paths, fields = [] }) => { beforeEach(async () => { ;({ container } = await renderResponse(server, { url: `${basePath}${paths.current}`, @@ -140,14 +157,30 @@ describe('User feedback journey', () => { })) }) - it('should render the page heading', () => { - const $heading = container.getByRole('heading', { - name: heading1, - level: 1 + it('dummy test in case no activated tests in this beforeEach loop', () => { + expect(paths.current).toBeDefined() + }) + + if (heading1) { + it('should render the page heading', () => { + const $heading = container.getByRole('heading', { + name: heading1, + level: 1 + }) + + expect($heading).toBeInTheDocument() }) + } - expect($heading).toBeInTheDocument() - }) + if (formName) { + it('should render the form title', () => { + const $title = container.getByRole('link', { + name: formName + }) + + expect($title).toBeInTheDocument() + }) + } if (paths.next) { it('should show errors when invalid on submit', async () => { From 6b3ca43536a39c19d9d81d3ae461a72e39a25c1c Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 1 Dec 2025 08:59:04 +0000 Subject: [PATCH 10/11] Rework after review --- src/server/plugins/FeedbackPageController.ts | 3 ++- src/server/plugins/router.ts | 27 +++++--------------- test/form/feedback.test.js | 14 ++++++++++ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/server/plugins/FeedbackPageController.ts b/src/server/plugins/FeedbackPageController.ts index 51fdbdbe7..9d125f726 100644 --- a/src/server/plugins/FeedbackPageController.ts +++ b/src/server/plugins/FeedbackPageController.ts @@ -12,6 +12,8 @@ import { } from '@defra/forms-engine-plugin/types' export class FeedbackPageController extends QuestionPageController { + allowSaveAndExit = false + getViewModel( request: FormContextRequest, context: FormContext @@ -19,7 +21,6 @@ export class FeedbackPageController extends QuestionPageController { const viewModel = super.getViewModel(request, context) return { ...viewModel, - allowSaveAndExit: false, hidePhaseBanner: true, submitButtonText: 'Send feedback', name: context.state.formName diff --git a/src/server/plugins/router.ts b/src/server/plugins/router.ts index c6eed1d90..f79dc0a04 100644 --- a/src/server/plugins/router.ts +++ b/src/server/plugins/router.ts @@ -1,4 +1,5 @@ import { + getCacheService, handleLegacyRedirect, isPathRelative } from '@defra/forms-engine-plugin/engine/helpers.js' @@ -8,7 +9,6 @@ import { pathSchema, stateSchema } from '@defra/forms-engine-plugin/schema.js' -import { FormStatus } from '@defra/forms-engine-plugin/types' import { slugSchema } from '@defra/forms-model' import Boom from '@hapi/boom' import { @@ -34,10 +34,7 @@ import { publicRoutes, saveAndExitRoutes } from '~/src/server/routes/index.js' -import { - getFormDefinition, - getFormMetadata -} from '~/src/server/services/formsService.js' +import { getFormMetadata } from '~/src/server/services/formsService.js' const routes: ServerRoute[] = [...publicRoutes, healthRoute] const saveAndExitExpiryDays = config.get('saveAndExitExpiryDays') @@ -155,29 +152,17 @@ export default { const sessionDurationPretty = humanizeDuration(sessionTimeout) - const { slug } = request.params + const cacheService = getCacheService(request.server) - let formId = '' - let formTitle - - if (slug) { - try { - const meta = await getFormMetadata(request.params.slug) - const definition = await getFormDefinition( - meta.id, - FormStatus.Draft - ) - formId = meta.id - formTitle = definition?.name - } catch {} - } + const state = await cacheService.getState(request) + + const formId = state?.formId ?? '' return h.view('help/cookies', { googleAnalyticsContainerId: config .get('googleAnalyticsTrackingId') .replace(/^G-/, ''), sessionDurationPretty, - name: formTitle, feedbackLink: `/form/feedback?formId=${formId}` }) }, diff --git a/test/form/feedback.test.js b/test/form/feedback.test.js index 84c602fc3..0e504972e 100644 --- a/test/form/feedback.test.js +++ b/test/form/feedback.test.js @@ -1,11 +1,17 @@ import { join } from 'node:path' +import { + checkFormStatus, + getCacheService +} from '@defra/forms-engine-plugin/engine/helpers.js' + import { createServer } from '~/src/server/index.js' import { getFormMetadata } from '~/src/server/services/formsService.js' import * as fixtures from '~/test/fixtures/index.js' import { renderResponse } from '~/test/helpers/component-helpers.js' jest.mock('~/src/server/services/formsService.js') +jest.mock('@defra/forms-engine-plugin/engine/helpers.js') describe('Feedback link', () => { /** @type {Server} */ @@ -30,6 +36,14 @@ describe('Feedback link', () => { }) it('should match route', async () => { + // @ts-expect-error - not all method mocked + jest.mocked(getCacheService).mockImplementationOnce(() => ({ + setState: jest.fn(), + getState: jest + .fn() + .mockResolvedValue({ formId: '661e4ca5039739ef2902b214' }) + })) + jest.mocked(checkFormStatus).mockReturnValue({ isPreview: true, state: {} }) const { container } = await renderResponse(server, { method: 'GET', url: '/help/cookies/feedback' From 2ae2ec52f5854741bd1c56097b3768335054a4cc Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Tue, 2 Dec 2025 12:09:03 +0000 Subject: [PATCH 11/11] remove feedback link env vars --- README.md | 34 ++++++++++++++++------------------ jest.setup.cjs | 1 - package-lock.json | 27 --------------------------- 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index aafcdeff0..8d11f4f57 100644 --- a/README.md +++ b/README.md @@ -174,23 +174,22 @@ Please use a config file instead. This will give you more control over each envi The defaults can be found in [config](./src/config/index.ts). Place your config files in `runner/config` See [https://github.com/node-config/node-config#readme](https://github.com/node-config/node-config#readme) for more info. -| name | description | required | default | valid | notes | -| --------------------- | -------------------------------------------------------------------------------- | :------: | ------- | :-------------------------: | :-------------------------------------------------------------------------------------------------------------------------: | -| NODE_ENV | Node environment | no | | development,test,production | | -| PORT | Port number | no | 3009 | | | -| NOTIFY_TEMPLATE_ID | Notify api key | yes | | | Template ID required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. | -| NOTIFY_API_KEY | Notify api key | yes | | | API KEY required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. | -| LOG_LEVEL | Log level | no | debug | trace,debug,info,error | | -| PHASE_TAG | Tag to use for phase banner | no | beta | alpha, beta, empty string | | -| FEEDBACK_LINK | Link to display in the phase banner when asking for feedback. | no | | | Used for an anchor tag's href. To display an email link, use a 'mailto:dest@domain.com' value. Else use a standard website. | -| HTTP_PROXY | HTTP proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | -| HTTPS_PROXY | HTTPS proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | -| NO_PROXY | HTTP proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | -| AWS_ACCESS_KEY_ID | AWS key id | yes | dummy | | | -| AWS_SECRET_ACCESS_KEY | AWS access key | yes | dummy | | | -| SNS_ENDPOINT | Endpoint for SNS messaging | yes | | | | -| SNS_ADAPTER_TOPIC_ARN | The SNS topic for the submission adapter - in Amazon Resource Name (ARN) format. | yes | | | | -| SNS_SAVE_TOPIC_ARN | The SNS topic for the save-and-exit - in Amazon Resource Name (ARN) format. | yes | | | | +| name | description | required | default | valid | notes | +| --------------------- | -------------------------------------------------------------------------------- | :------: | ------- | :-------------------------: | :---------------------------------------------------------------------------------------------------------------------: | +| NODE_ENV | Node environment | no | | development,test,production | | +| PORT | Port number | no | 3009 | | | +| NOTIFY_TEMPLATE_ID | Notify api key | yes | | | Template ID required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. | +| NOTIFY_API_KEY | Notify api key | yes | | | API KEY required to send form payloads via [GOV.UK Notify](https://www.notifications.service.gov.uk) email service. | +| LOG_LEVEL | Log level | no | debug | trace,debug,info,error | | +| PHASE_TAG | Tag to use for phase banner | no | beta | alpha, beta, empty string | | +| HTTP_PROXY | HTTP proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | +| HTTPS_PROXY | HTTPS proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | +| NO_PROXY | HTTP proxy to use, e.g. the one from CDP. Currently used for Hapi Wreck. | no | | | | +| AWS_ACCESS_KEY_ID | AWS key id | yes | dummy | | | +| AWS_SECRET_ACCESS_KEY | AWS access key | yes | dummy | | | +| SNS_ENDPOINT | Endpoint for SNS messaging | yes | | | | +| SNS_ADAPTER_TOPIC_ARN | The SNS topic for the submission adapter - in Amazon Resource Name (ARN) format. | yes | | | | +| SNS_SAVE_TOPIC_ARN | The SNS topic for the save-and-exit - in Amazon Resource Name (ARN) format. | yes | | | | For proxy options, see https://www.npmjs.com/package/proxy-from-env which is used by https://github.com/TooTallNate/proxy-agents/tree/main/packages/proxy-agent. @@ -207,7 +206,6 @@ MANAGER_URL=http://localhost:3001 BASE_URL=http://localhost:3009 NOTIFY_TEMPLATE_ID= NOTIFY_API_KEY= -FEEDBACK_LINK=http://test.com DESIGNER_URL=http://localhost:3000 SUBMISSION_URL=http://localhost:3002 UPLOADER_BUCKET_NAME=my-bucket diff --git a/jest.setup.cjs b/jest.setup.cjs index db16c6a47..b30216847 100644 --- a/jest.setup.cjs +++ b/jest.setup.cjs @@ -6,7 +6,6 @@ process.env.REDIS_USERNAME = 'dummy' process.env.SESSION_COOKIE_PASSWORD = 'test-env-session-cookie-password' process.env.NOTIFY_TEMPLATE_ID = 'dummy' process.env.NOTIFY_API_KEY = 'dummy' -process.env.FEEDBACK_LINK = 'https://test.defra.gov.uk/' process.env.DESIGNER_URL = 'https://forms-designer' process.env.SUBMISSION_URL = 'https://test-submission-api.cdp-int.defra.cloud' process.env.UPLOADER_URL = 'https://test-uploader.cdp-int.defra.cloud' diff --git a/package-lock.json b/package-lock.json index 7fd1bc8a1..b5a3f29f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -861,7 +861,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2718,7 +2717,6 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2832,7 +2830,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2856,7 +2853,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -6610,7 +6606,6 @@ "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.47.0", @@ -6641,7 +6636,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -7426,7 +7420,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7892,7 +7885,6 @@ "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/sinon": "^17.0.3", "sinon": "^18.0.1", @@ -8308,7 +8300,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -10065,7 +10056,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10316,7 +10306,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10490,7 +10479,6 @@ "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", @@ -10554,7 +10542,6 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -12631,7 +12618,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -13719,7 +13705,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -13729,7 +13714,6 @@ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -13798,7 +13782,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -15415,7 +15398,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15979,7 +15961,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -17228,7 +17209,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18023,7 +18003,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -18759,7 +18738,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18925,7 +18903,6 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -19060,7 +19037,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19146,7 +19122,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -19347,7 +19322,6 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19416,7 +19390,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1",