From c7b6a11edfdb4cec7696a7112cdbf6c6bd7a652a Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 11:10:28 +0100 Subject: [PATCH 01/13] feat: add NotificationBanner content component --- .../components/NotificationBanner.test.ts | 85 +++++++++++++++++++ .../engine/components/NotificationBanner.ts | 31 +++++++ .../engine/components/helpers/components.ts | 5 ++ src/server/plugins/engine/components/index.ts | 1 + 4 files changed, 122 insertions(+) create mode 100644 src/server/plugins/engine/components/NotificationBanner.test.ts create mode 100644 src/server/plugins/engine/components/NotificationBanner.ts diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts new file mode 100644 index 000000000..7c2179666 --- /dev/null +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -0,0 +1,85 @@ +import { + ComponentType, + type NotificationBannerComponent +} from '@defra/forms-model' + +import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' +import { type Guidance } from '~/src/server/plugins/engine/components/helpers/components.js' +import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' +import definition from '~/test/form/definitions/basic.js' + +describe('NotificationBanner', () => { + let model: FormModel + + beforeEach(() => { + model = new FormModel(definition, { + basePath: 'test' + }) + }) + + describe('Defaults', () => { + let def: NotificationBannerComponent + let collection: ComponentCollection + let guidance: Guidance + + beforeEach(() => { + def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'You have 30 days to appeal this decision.', + options: {} + } satisfies NotificationBannerComponent + + collection = new ComponentCollection([def], { model }) + guidance = collection.guidance[0] + }) + + describe('View model', () => { + it('sets Nunjucks component defaults', () => { + const viewModel = guidance.getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + attributes: {}, + titleHtml: def.title, + html: def.content + }) + ) + }) + }) + }) + + describe('Success variant', () => { + let def: NotificationBannerComponent + let collection: ComponentCollection + let guidance: Guidance + + beforeEach(() => { + def = { + title: 'Success', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Your application has been submitted.', + options: { type: 'success' } + } satisfies NotificationBannerComponent + + collection = new ComponentCollection([def], { model }) + guidance = collection.guidance[0] + }) + + describe('View model', () => { + it('includes type: success', () => { + const viewModel = guidance.getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + html: def.content, + type: 'success' + }) + ) + }) + }) + }) +}) diff --git a/src/server/plugins/engine/components/NotificationBanner.ts b/src/server/plugins/engine/components/NotificationBanner.ts new file mode 100644 index 000000000..03695e911 --- /dev/null +++ b/src/server/plugins/engine/components/NotificationBanner.ts @@ -0,0 +1,31 @@ +import { type NotificationBannerComponent } from '@defra/forms-model' + +import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js' + +export class NotificationBanner extends ComponentBase { + declare options: NotificationBannerComponent['options'] + content: NotificationBannerComponent['content'] + + constructor( + def: NotificationBannerComponent, + props: ConstructorParameters[1] + ) { + super(def, props) + + const { content, options } = def + + this.content = content + this.options = options + } + + getViewModel() { + const { content, title, viewModel } = this + + return { + ...viewModel, + titleHtml: title, + html: content, + type: this.options.type + } + } +} diff --git a/src/server/plugins/engine/components/helpers/components.ts b/src/server/plugins/engine/components/helpers/components.ts index 87e1cc264..fbda7be88 100644 --- a/src/server/plugins/engine/components/helpers/components.ts +++ b/src/server/plugins/engine/components/helpers/components.ts @@ -46,6 +46,7 @@ export type Guidance = | InstanceType | InstanceType | InstanceType + | InstanceType // List component instances only export type ListField = InstanceType< @@ -134,6 +135,10 @@ export function createComponent( component = new Components.Markdown(def, options) break + case ComponentType.NotificationBanner: + component = new Components.NotificationBanner(def, options) + break + case ComponentType.MultilineTextField: component = new Components.MultilineTextField(def, options) break diff --git a/src/server/plugins/engine/components/index.ts b/src/server/plugins/engine/components/index.ts index 7b59f6b11..8662e8ad2 100644 --- a/src/server/plugins/engine/components/index.ts +++ b/src/server/plugins/engine/components/index.ts @@ -15,6 +15,7 @@ export { Html } from '~/src/server/plugins/engine/components/Html.js' export { InsetText } from '~/src/server/plugins/engine/components/InsetText.js' export { List } from '~/src/server/plugins/engine/components/List.js' export { Markdown } from '~/src/server/plugins/engine/components/Markdown.js' +export { NotificationBanner } from '~/src/server/plugins/engine/components/NotificationBanner.js' export { MonthYearField } from '~/src/server/plugins/engine/components/MonthYearField.js' export { MultilineTextField } from '~/src/server/plugins/engine/components/MultilineTextField.js' export { NumberField } from '~/src/server/plugins/engine/components/NumberField.js' From d3daa3961c3897ce947db089bff9b71e7abb061f Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 11:12:04 +0100 Subject: [PATCH 02/13] feat: add NotificationBanner Nunjucks template --- .../plugins/engine/views/components/notificationbanner.html | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/server/plugins/engine/views/components/notificationbanner.html diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html new file mode 100644 index 000000000..f04336622 --- /dev/null +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -0,0 +1,5 @@ +{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} + +{% macro NotificationBanner(component) %} + {{ govukNotificationBanner(component.model) }} +{% endmacro %} From f041c9155042c31ef672e6e1356fa8c12dab0c5e Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 11:13:11 +0100 Subject: [PATCH 03/13] chore: add NotificationBanner demo page to simple-form --- src/server/forms/simple-form.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index efa8f8215..010f3fe07 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -69,6 +69,24 @@ pages: id: '00738799-3489-4ab2-a57b-542eecb31bfa' next: [] id: da0fbdb4-a2de-4650-be16-9ba552af135f + - title: '' + path: '/notification-demo' + components: + - type: NotificationBanner + title: Important + name: notificationImportant + content: '

You have 30 days to appeal this decision.

' + options: {} + id: a3f8e1b2-4c56-7d89-0e12-3f456789abcd + - type: NotificationBanner + title: Application submitted + name: notificationSuccess + content: '

Your application for {{ applicantFirstName }} {{ applicantLastName }} has been received.

' + options: + type: success + id: b4e9f2c3-5d67-8e90-1f23-4g567890bcde + next: [] + id: c5f0a3d4-6e78-9f01-2a34-5h678901cdef - id: 449a45f6-4541-4a46-91bd-8b8931b07b50 title: '' path: '/summary' From 0404eb6ebd0ea1e16f3d063ead246016e7dd788f Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 11:42:55 +0100 Subject: [PATCH 04/13] feat: render notification banner content via markdown parser --- src/server/forms/simple-form.yaml | 10 +++++----- .../engine/components/NotificationBanner.test.ts | 6 +++--- .../plugins/engine/components/NotificationBanner.ts | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index 010f3fe07..4e0c1cbd3 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -75,18 +75,18 @@ pages: - type: NotificationBanner title: Important name: notificationImportant - content: '

You have 30 days to appeal this decision.

' + content: 'You have 30 days to appeal this decision.' options: {} - id: a3f8e1b2-4c56-7d89-0e12-3f456789abcd + id: ca6b6590-1608-49f4-82f9-9d92e38e66ea - type: NotificationBanner title: Application submitted name: notificationSuccess - content: '

Your application for {{ applicantFirstName }} {{ applicantLastName }} has been received.

' + content: 'Your application for {{ applicantFirstName }} {{ applicantLastName }} has been received.' options: type: success - id: b4e9f2c3-5d67-8e90-1f23-4g567890bcde + id: b6960f4b-d771-4956-9c9b-17da5c603062 next: [] - id: c5f0a3d4-6e78-9f01-2a34-5h678901cdef + id: be4b4b86-514c-43bb-8cdc-f9aed6155924 - id: 449a45f6-4541-4a46-91bd-8b8931b07b50 title: '' path: '/summary' diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index 7c2179666..3d301db6f 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -27,7 +27,7 @@ describe('NotificationBanner', () => { title: 'Important', name: 'myComponent', type: ComponentType.NotificationBanner, - content: 'You have 30 days to appeal this decision.', + content: 'You have 30 days to [appeal this decision](/appeal).', options: {} } satisfies NotificationBannerComponent @@ -43,7 +43,7 @@ describe('NotificationBanner', () => { expect.objectContaining({ attributes: {}, titleHtml: def.title, - html: def.content + html: 'You have 30 days to appeal this decision.
' }) ) }) @@ -75,7 +75,7 @@ describe('NotificationBanner', () => { expect(viewModel).toEqual( expect.objectContaining({ titleHtml: def.title, - html: def.content, + html: 'Your application has been submitted.', type: 'success' }) ) diff --git a/src/server/plugins/engine/components/NotificationBanner.ts b/src/server/plugins/engine/components/NotificationBanner.ts index 03695e911..3ddf10460 100644 --- a/src/server/plugins/engine/components/NotificationBanner.ts +++ b/src/server/plugins/engine/components/NotificationBanner.ts @@ -1,6 +1,7 @@ import { type NotificationBannerComponent } from '@defra/forms-model' import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js' +import { markdown } from '~/src/server/plugins/engine/components/markdownParser.js' export class NotificationBanner extends ComponentBase { declare options: NotificationBannerComponent['options'] @@ -24,7 +25,7 @@ export class NotificationBanner extends ComponentBase { return { ...viewModel, titleHtml: title, - html: content, + html: markdown.parse(content, { async: false }).trim(), type: this.options.type } } From a3cc57e58d669bad69861c81c3f37ceccdf64454 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 11:54:03 +0100 Subject: [PATCH 05/13] fix: use Nunjucks markdown filter for banner content rendering --- .../plugins/engine/components/NotificationBanner.test.ts | 6 +++--- src/server/plugins/engine/components/NotificationBanner.ts | 3 +-- .../engine/views/components/notificationbanner.html | 7 ++++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index 3d301db6f..4446fbde7 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -27,7 +27,7 @@ describe('NotificationBanner', () => { title: 'Important', name: 'myComponent', type: ComponentType.NotificationBanner, - content: 'You have 30 days to [appeal this decision](/appeal).', + content: 'You have 30 days to appeal this decision.', options: {} } satisfies NotificationBannerComponent @@ -43,7 +43,7 @@ describe('NotificationBanner', () => { expect.objectContaining({ attributes: {}, titleHtml: def.title, - html: 'You have 30 days to appeal this decision.
' + content: def.content }) ) }) @@ -75,7 +75,7 @@ describe('NotificationBanner', () => { expect(viewModel).toEqual( expect.objectContaining({ titleHtml: def.title, - html: 'Your application has been submitted.', + content: def.content, type: 'success' }) ) diff --git a/src/server/plugins/engine/components/NotificationBanner.ts b/src/server/plugins/engine/components/NotificationBanner.ts index 3ddf10460..6bed16a1f 100644 --- a/src/server/plugins/engine/components/NotificationBanner.ts +++ b/src/server/plugins/engine/components/NotificationBanner.ts @@ -1,7 +1,6 @@ import { type NotificationBannerComponent } from '@defra/forms-model' import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js' -import { markdown } from '~/src/server/plugins/engine/components/markdownParser.js' export class NotificationBanner extends ComponentBase { declare options: NotificationBannerComponent['options'] @@ -25,7 +24,7 @@ export class NotificationBanner extends ComponentBase { return { ...viewModel, titleHtml: title, - html: markdown.parse(content, { async: false }).trim(), + content, type: this.options.type } } diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html index f04336622..a9728703a 100644 --- a/src/server/plugins/engine/views/components/notificationbanner.html +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -1,5 +1,10 @@ {% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} {% macro NotificationBanner(component) %} - {{ govukNotificationBanner(component.model) }} + {% set bannerHtml %}{{ component.model.content | markdown | safe }}{% endset %} + {{ govukNotificationBanner({ + titleHtml: component.model.titleHtml, + html: bannerHtml, + type: component.model.type + }) }} {% endmacro %} From 2efe8c9663b83d6122644226ee4e4599906a6ef1 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 12:02:08 +0100 Subject: [PATCH 06/13] test: use markdown content in NotificationBanner test fixture --- src/server/plugins/engine/components/NotificationBanner.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index 4446fbde7..e35903779 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -27,7 +27,7 @@ describe('NotificationBanner', () => { title: 'Important', name: 'myComponent', type: ComponentType.NotificationBanner, - content: 'You have 30 days to appeal this decision.', + content: 'You have 30 days to [appeal this decision](/appeal).', options: {} } satisfies NotificationBannerComponent From cb8a24bd126922f97c870af9e99003e648cb9321 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 12:26:54 +0100 Subject: [PATCH 07/13] feat: add heading option and fix template whitespace on notification banner --- src/server/forms/simple-form.yaml | 9 ++--- .../components/NotificationBanner.test.ts | 35 +++++++++++++++++++ .../engine/components/NotificationBanner.ts | 1 + .../views/components/notificationbanner.html | 5 ++- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index 4e0c1cbd3..f64d7cd94 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -75,13 +75,14 @@ pages: - type: NotificationBanner title: Important name: notificationImportant - content: 'You have 30 days to appeal this decision.' - options: {} + content: 'Contact us if you need [help with your application](/help).' + options: + heading: There may be a delay in processing your application. id: ca6b6590-1608-49f4-82f9-9d92e38e66ea - type: NotificationBanner - title: Application submitted + title: Success name: notificationSuccess - content: 'Your application for {{ applicantFirstName }} {{ applicantLastName }} has been received.' + content: 'Your application has been submitted.' options: type: success id: b6960f4b-d771-4956-9c9b-17da5c603062 diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index e35903779..f6ed5eaa2 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -50,6 +50,41 @@ describe('NotificationBanner', () => { }) }) + describe('With heading', () => { + let def: NotificationBannerComponent + let collection: ComponentCollection + let guidance: Guidance + + beforeEach(() => { + def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Contact us if you need help.', + options: { + heading: 'There may be a delay in processing your application.' + } + } satisfies NotificationBannerComponent + + collection = new ComponentCollection([def], { model }) + guidance = collection.guidance[0] + }) + + describe('View model', () => { + it('includes heading', () => { + const viewModel = guidance.getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + content: def.content, + heading: def.options.heading + }) + ) + }) + }) + }) + describe('Success variant', () => { let def: NotificationBannerComponent let collection: ComponentCollection diff --git a/src/server/plugins/engine/components/NotificationBanner.ts b/src/server/plugins/engine/components/NotificationBanner.ts index 6bed16a1f..2ee78cf3b 100644 --- a/src/server/plugins/engine/components/NotificationBanner.ts +++ b/src/server/plugins/engine/components/NotificationBanner.ts @@ -25,6 +25,7 @@ export class NotificationBanner extends ComponentBase { ...viewModel, titleHtml: title, content, + heading: this.options.heading, type: this.options.type } } diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html index a9728703a..9907a0a5f 100644 --- a/src/server/plugins/engine/views/components/notificationbanner.html +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -1,7 +1,10 @@ {% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} {% macro NotificationBanner(component) %} - {% set bannerHtml %}{{ component.model.content | markdown | safe }}{% endset %} + {%- set bannerHtml -%} + {%- if component.model.heading %}

{{ component.model.heading }}

{% endif -%} + {{ component.model.content | markdown | safe }} + {%- endset -%} {{ govukNotificationBanner({ titleHtml: component.model.titleHtml, html: bannerHtml, From 0daddc04219f94c83a5b8998123e9b4a8c9fdf80 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 13:33:53 +0100 Subject: [PATCH 08/13] fix: wrap banner markdown content in app-prose-scope for correct paragraph styling --- .../plugins/engine/views/components/notificationbanner.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html index 9907a0a5f..319347f00 100644 --- a/src/server/plugins/engine/views/components/notificationbanner.html +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -3,7 +3,7 @@ {% macro NotificationBanner(component) %} {%- set bannerHtml -%} {%- if component.model.heading %}

{{ component.model.heading }}

{% endif -%} - {{ component.model.content | markdown | safe }} +
{{ component.model.content | markdown | safe }}
{%- endset -%} {{ govukNotificationBanner({ titleHtml: component.model.titleHtml, From 41a56a622ef708dcdc6290f2139ac2cbd5528726 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 13:42:53 +0100 Subject: [PATCH 09/13] fix: remove trailing margin from last child in app-prose-scope --- src/client/stylesheets/_prose.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/stylesheets/_prose.scss b/src/client/stylesheets/_prose.scss index a929dceba..ec7801fe7 100644 --- a/src/client/stylesheets/_prose.scss +++ b/src/client/stylesheets/_prose.scss @@ -26,6 +26,10 @@ @extend %govuk-body-m; } + > :last-child { + margin-bottom: 0; + } + strong, b { @include govuk-typography-weight-bold; From fc96994a4191bba679393852f5350f981bb99181 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 15:24:35 +0100 Subject: [PATCH 10/13] chore: update demo link to defra.gov.uk --- src/server/forms/simple-form.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index f64d7cd94..74d00a8a0 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -75,7 +75,7 @@ pages: - type: NotificationBanner title: Important name: notificationImportant - content: 'Contact us if you need [help with your application](/help).' + content: 'Contact us if you need [help with your application](https://www.defra.gov.uk).' options: heading: There may be a delay in processing your application. id: ca6b6590-1608-49f4-82f9-9d92e38e66ea From ae1cea8110b5cb3d191df24c717593fabc279aab Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 15:28:27 +0100 Subject: [PATCH 11/13] refactor: inline test setup into individual NotificationBanner tests --- .../components/NotificationBanner.test.ts | 89 +++++++------------ 1 file changed, 32 insertions(+), 57 deletions(-) diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index f6ed5eaa2..8a01be296 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -4,7 +4,6 @@ import { } from '@defra/forms-model' import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' -import { type Guidance } from '~/src/server/plugins/engine/components/helpers/components.js' import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' import definition from '~/test/form/definitions/basic.js' @@ -18,26 +17,18 @@ describe('NotificationBanner', () => { }) describe('Defaults', () => { - let def: NotificationBannerComponent - let collection: ComponentCollection - let guidance: Guidance - - beforeEach(() => { - def = { - title: 'Important', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'You have 30 days to [appeal this decision](/appeal).', - options: {} - } satisfies NotificationBannerComponent - - collection = new ComponentCollection([def], { model }) - guidance = collection.guidance[0] - }) - describe('View model', () => { it('sets Nunjucks component defaults', () => { - const viewModel = guidance.getViewModel() + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'You have 30 days to [appeal this decision](/appeal).', + options: {} + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() expect(viewModel).toEqual( expect.objectContaining({ @@ -51,28 +42,20 @@ describe('NotificationBanner', () => { }) describe('With heading', () => { - let def: NotificationBannerComponent - let collection: ComponentCollection - let guidance: Guidance - - beforeEach(() => { - def = { - title: 'Important', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'Contact us if you need help.', - options: { - heading: 'There may be a delay in processing your application.' - } - } satisfies NotificationBannerComponent - - collection = new ComponentCollection([def], { model }) - guidance = collection.guidance[0] - }) - describe('View model', () => { it('includes heading', () => { - const viewModel = guidance.getViewModel() + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Contact us if you need help.', + options: { + heading: 'There may be a delay in processing your application.' + } + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() expect(viewModel).toEqual( expect.objectContaining({ @@ -86,26 +69,18 @@ describe('NotificationBanner', () => { }) describe('Success variant', () => { - let def: NotificationBannerComponent - let collection: ComponentCollection - let guidance: Guidance - - beforeEach(() => { - def = { - title: 'Success', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'Your application has been submitted.', - options: { type: 'success' } - } satisfies NotificationBannerComponent - - collection = new ComponentCollection([def], { model }) - guidance = collection.guidance[0] - }) - describe('View model', () => { it('includes type: success', () => { - const viewModel = guidance.getViewModel() + const def = { + title: 'Success', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Your application has been submitted.', + options: { type: 'success' } + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() expect(viewModel).toEqual( expect.objectContaining({ From 6e8d81747848b327f695dd1ee9735d8f7a461b93 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 15:30:03 +0100 Subject: [PATCH 12/13] feat: support markdown in notification banner heading via app-prose-scope --- .../plugins/engine/components/NotificationBanner.test.ts | 2 +- .../plugins/engine/views/components/notificationbanner.html | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index 8a01be296..84d632501 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -50,7 +50,7 @@ describe('NotificationBanner', () => { type: ComponentType.NotificationBanner, content: 'Contact us if you need help.', options: { - heading: 'There may be a delay in processing your application.' + heading: 'There may be a **delay** in processing your application.' } } satisfies NotificationBannerComponent diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html index 319347f00..8aaa7fb78 100644 --- a/src/server/plugins/engine/views/components/notificationbanner.html +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -2,8 +2,10 @@ {% macro NotificationBanner(component) %} {%- set bannerHtml -%} - {%- if component.model.heading %}

{{ component.model.heading }}

{% endif -%} -
{{ component.model.content | markdown | safe }}
+
+ {%- if component.model.heading %}{{ component.model.heading | markdown | safe }}{% endif -%} + {{ component.model.content | markdown | safe }} +
{%- endset -%} {{ govukNotificationBanner({ titleHtml: component.model.titleHtml, From ab1d3048cce31f94c28fbb31c7a7cc580f8473e0 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Mon, 18 May 2026 16:02:28 +0100 Subject: [PATCH 13/13] refactor: flatten NotificationBanner tests, remove single-test describe blocks --- .../components/NotificationBanner.test.ts | 112 ++++++++---------- 1 file changed, 50 insertions(+), 62 deletions(-) diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts index 84d632501..de8efd586 100644 --- a/src/server/plugins/engine/components/NotificationBanner.test.ts +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -16,80 +16,68 @@ describe('NotificationBanner', () => { }) }) - describe('Defaults', () => { - describe('View model', () => { - it('sets Nunjucks component defaults', () => { - const def = { - title: 'Important', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'You have 30 days to [appeal this decision](/appeal).', - options: {} - } satisfies NotificationBannerComponent + it('sets Nunjucks component defaults', () => { + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'You have 30 days to [appeal this decision](/appeal).', + options: {} + } satisfies NotificationBannerComponent - const collection = new ComponentCollection([def], { model }) - const viewModel = collection.guidance[0].getViewModel() + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() - expect(viewModel).toEqual( - expect.objectContaining({ - attributes: {}, - titleHtml: def.title, - content: def.content - }) - ) + expect(viewModel).toEqual( + expect.objectContaining({ + attributes: {}, + titleHtml: def.title, + content: def.content }) - }) + ) }) - describe('With heading', () => { - describe('View model', () => { - it('includes heading', () => { - const def = { - title: 'Important', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'Contact us if you need help.', - options: { - heading: 'There may be a **delay** in processing your application.' - } - } satisfies NotificationBannerComponent + it('includes heading in view model', () => { + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Contact us if you need help.', + options: { + heading: 'There may be a **delay** in processing your application.' + } + } satisfies NotificationBannerComponent - const collection = new ComponentCollection([def], { model }) - const viewModel = collection.guidance[0].getViewModel() + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() - expect(viewModel).toEqual( - expect.objectContaining({ - titleHtml: def.title, - content: def.content, - heading: def.options.heading - }) - ) + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + content: def.content, + heading: def.options.heading }) - }) + ) }) - describe('Success variant', () => { - describe('View model', () => { - it('includes type: success', () => { - const def = { - title: 'Success', - name: 'myComponent', - type: ComponentType.NotificationBanner, - content: 'Your application has been submitted.', - options: { type: 'success' } - } satisfies NotificationBannerComponent + it('sets type: success for success variant', () => { + const def = { + title: 'Success', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Your application has been submitted.', + options: { type: 'success' } + } satisfies NotificationBannerComponent - const collection = new ComponentCollection([def], { model }) - const viewModel = collection.guidance[0].getViewModel() + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() - expect(viewModel).toEqual( - expect.objectContaining({ - titleHtml: def.title, - content: def.content, - type: 'success' - }) - ) + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + content: def.content, + type: 'success' }) - }) + ) }) })