From 53e6105a900d4a55f355525c9c5f87c9c2ed631a Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 10:59:37 +0000 Subject: [PATCH 01/23] CCM-11494 Routing utils --- .../get-message-plan-template-ids.test.ts | 65 --- .../src/__tests__/utils/routing-utils.test.ts | 529 ++++++++++++++++++ .../utils/get-message-plan-template-ids.ts | 23 - frontend/src/utils/message-plans.ts | 14 +- frontend/src/utils/routing-utils.ts | 163 ++++++ 5 files changed, 702 insertions(+), 92 deletions(-) delete mode 100644 frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts create mode 100644 frontend/src/__tests__/utils/routing-utils.test.ts delete mode 100644 frontend/src/utils/get-message-plan-template-ids.ts create mode 100644 frontend/src/utils/routing-utils.ts diff --git a/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts b/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts deleted file mode 100644 index 1ce2e709d..000000000 --- a/frontend/src/__tests__/utils/get-message-plan-template-ids.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { getMessagePlanTemplateIds } from '@utils/get-message-plan-template-ids'; -import type { RoutingConfig } from 'nhs-notify-backend-client'; - -const baseConfig: RoutingConfig = { - id: 'test-id', - name: 'Test message plan', - status: 'DRAFT', - clientId: 'client-1', - campaignId: 'campaign-1', - createdAt: '2025-01-01T00:00:00.000Z', - updatedAt: '2025-01-01T00:00:00.000Z', - cascade: [], - cascadeGroupOverrides: [{ name: 'standard' }], -}; - -describe('getMessagePlanTemplateIds', () => { - it('should collect unique template IDs from defaults and conditionals', () => { - const plan: RoutingConfig = { - ...baseConfig, - cascade: [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: 'template-1', - }, - { - cascadeGroups: ['standard'], - channel: 'EMAIL', - channelType: 'primary', - conditionalTemplates: [ - { templateId: 'template-2', language: 'fr' }, - { templateId: 'template-3', language: 'fr' }, - ], - }, - { - cascadeGroups: ['standard'], - channel: 'SMS', - channelType: 'primary', - defaultTemplateId: 'template-1', - conditionalTemplates: [{ templateId: 'template-2', language: 'fr' }], - }, - ], - }; - - const ids = [...getMessagePlanTemplateIds(plan)].sort(); - expect(ids).toEqual(['template-1', 'template-2', 'template-3']); - }); - - it('should return empty set when there are no templates', () => { - const plan: RoutingConfig = { - ...baseConfig, - cascade: [ - { - cascadeGroups: ['standard'], - channel: 'NHSAPP', - channelType: 'primary', - defaultTemplateId: '', - }, - ], - }; - const ids = getMessagePlanTemplateIds(plan); - expect(ids.size).toBe(0); - }); -}); diff --git a/frontend/src/__tests__/utils/routing-utils.test.ts b/frontend/src/__tests__/utils/routing-utils.test.ts new file mode 100644 index 000000000..073c80b29 --- /dev/null +++ b/frontend/src/__tests__/utils/routing-utils.test.ts @@ -0,0 +1,529 @@ +import { + getMessagePlanTemplateIds, + shouldRemoveTemplate, + removeTemplatesFromConditionalTemplates, + removeTemplatesFromCascadeItem, + getRemainingAccessibleFormats, + getRemainingLanguages, + updateCascadeGroupOverrides, + type ConditionalTemplate, +} from '@utils/routing-utils'; +import type { + CascadeItem, + Language, + LetterType, + RoutingConfig, +} from 'nhs-notify-backend-client'; + +const baseConfig: RoutingConfig = { + id: 'test-id', + name: 'Test message plan', + status: 'DRAFT', + clientId: 'client-1', + campaignId: 'campaign-1', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-01T00:00:00.000Z', + cascade: [], + cascadeGroupOverrides: [{ name: 'standard' }], +}; + +describe('getMessagePlanTemplateIds', () => { + it('should collect unique template IDs from defaults and conditionals', () => { + const plan: RoutingConfig = { + ...baseConfig, + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + conditionalTemplates: [ + { templateId: 'template-2', language: 'fr' }, + { templateId: 'template-3', language: 'fr' }, + ], + }, + { + cascadeGroups: ['standard'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: 'template-1', + conditionalTemplates: [{ templateId: 'template-2', language: 'fr' }], + }, + ], + }; + + const ids = [...getMessagePlanTemplateIds(plan)].sort(); + expect(ids).toEqual(['template-1', 'template-2', 'template-3']); + }); + + it('should return empty set when there are no templates', () => { + const plan: RoutingConfig = { + ...baseConfig, + cascade: [ + { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: '', + }, + ], + }; + const ids = getMessagePlanTemplateIds(plan); + expect(ids.size).toBe(0); + }); +}); + +describe('shouldRemoveTemplate', () => { + it('should return true when template ID is in removal list', () => { + expect( + shouldRemoveTemplate('template-1', ['template-1', 'template-2']) + ).toBe(true); + }); + + it('should return false when template ID is not in removal list', () => { + expect( + shouldRemoveTemplate('template-3', ['template-1', 'template-2']) + ).toBe(false); + }); + + it('should return false when template ID is null', () => { + expect(shouldRemoveTemplate(null, ['template-1', 'template-2'])).toBe( + false + ); + }); + + it('should return false when template ID is undefined', () => { + expect(shouldRemoveTemplate(undefined, ['template-1', 'template-2'])).toBe( + false + ); + }); + + it('should return false when template ID is empty string', () => { + expect(shouldRemoveTemplate('', ['template-1', 'template-2'])).toBe(false); + }); +}); + +describe('removeTemplatesFromConditionalTemplates', () => { + it('should remove templates matching the IDs to remove', () => { + const conditionalTemplates: ConditionalTemplate[] = [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + { templateId: 'template-3', accessibleFormat: 'q4' }, + ]; + + const result = removeTemplatesFromConditionalTemplates( + conditionalTemplates, + ['template-1', 'template-3'] + ); + + expect(result).toEqual([{ templateId: 'template-2', language: 'es' }]); + }); + + it('should keep templates with null templateId', () => { + const conditionalTemplates: ConditionalTemplate[] = [ + { templateId: 'template-1', language: 'fr' }, + { templateId: null, language: 'es' }, + ]; + + const result = removeTemplatesFromConditionalTemplates( + conditionalTemplates, + ['template-1'] + ); + + expect(result).toEqual([{ templateId: null, language: 'es' }]); + }); + + it('should return empty array when all templates are removed', () => { + const conditionalTemplates: ConditionalTemplate[] = [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + ]; + + const result = removeTemplatesFromConditionalTemplates( + conditionalTemplates, + ['template-1', 'template-2'] + ); + + expect(result).toEqual([]); + }); +}); + +describe('removeTemplatesFromCascadeItem', () => { + it('should remove default template when in removal list', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }; + + const result = removeTemplatesFromCascadeItem(cascadeItem, ['template-1']); + + expect(result.defaultTemplateId).toBeNull(); + }); + + it('should keep default template when not in removal list', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }; + + const result = removeTemplatesFromCascadeItem(cascadeItem, ['template-2']); + + expect(result.defaultTemplateId).toBe('template-1'); + }); + + it('should remove conditional templates in removal list', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['translations'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + { templateId: 'template-3', language: 'de' }, + ], + }; + + const result = removeTemplatesFromCascadeItem(cascadeItem, [ + 'template-1', + 'template-3', + ]); + + expect(result.conditionalTemplates).toHaveLength(1); + expect(result.conditionalTemplates).toEqual([ + { templateId: 'template-2', language: 'es' }, + ]); + }); + + it('should remove both default and conditional templates', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['translations'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: 'template-default', + conditionalTemplates: [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + ], + }; + + const result = removeTemplatesFromCascadeItem(cascadeItem, [ + 'template-default', + 'template-1', + ]); + + expect(result.defaultTemplateId).toBeNull(); + expect(result.conditionalTemplates).toHaveLength(1); + expect(result.conditionalTemplates).toEqual([ + { templateId: 'template-2', language: 'es' }, + ]); + }); + + it('should handle cascade item with no conditional templates', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['standard'], + channel: 'NHSAPP', + channelType: 'primary', + defaultTemplateId: 'template-1', + }; + + const result = removeTemplatesFromCascadeItem(cascadeItem, ['template-2']); + + expect(result.defaultTemplateId).toBe('template-1'); + expect(result.conditionalTemplates).toBeUndefined(); + }); + + it('should not mutate the original cascade item', () => { + const cascadeItem: CascadeItem = { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + conditionalTemplates: [{ templateId: 'template-2', language: 'fr' }], + }; + + const originalDefaultId = cascadeItem.defaultTemplateId; + const originalConditionalLength = cascadeItem.conditionalTemplates?.length; + + removeTemplatesFromCascadeItem(cascadeItem, ['template-1', 'template-2']); + + expect(cascadeItem.defaultTemplateId).toBe(originalDefaultId); + expect(cascadeItem.conditionalTemplates?.length).toBe( + originalConditionalLength + ); + }); +}); + +describe('getRemainingAccessibleFormats', () => { + it('should collect all unique accessible format types', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['accessible'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', accessibleFormat: 'q4' }, + { templateId: 'template-2', accessibleFormat: 'x0' }, + ], + }, + { + cascadeGroups: ['accessible'], + channel: 'LETTER', + channelType: 'secondary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-3', accessibleFormat: 'q4' }, + ], + }, + ]; + + const result = getRemainingAccessibleFormats(cascade); + + expect(result.sort()).toEqual(['q4', 'x0'].sort()); + }); + + it('should ignore templates with null templateId', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['accessible'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', accessibleFormat: 'q4' }, + { templateId: null, accessibleFormat: 'x0' }, + ], + }, + ]; + + const result = getRemainingAccessibleFormats(cascade); + + expect(result).toEqual(['q4']); + }); + + it('should return empty array when no accessible templates exist', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + ]; + + const result = getRemainingAccessibleFormats(cascade); + + expect(result).toEqual([]); + }); +}); + +describe('getRemainingLanguages', () => { + it('should collect all unique language types', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['translations'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + ], + }, + { + cascadeGroups: ['translations'], + channel: 'SMS', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [{ templateId: 'template-3', language: 'fr' }], + }, + ]; + + const result = getRemainingLanguages(cascade); + + expect(result.sort()).toEqual(['es', 'fr']); + }); + + it('should ignore templates with null templateId', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['translations'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', language: 'fr' }, + { templateId: null, language: 'es' }, + ], + }, + ]; + + const result = getRemainingLanguages(cascade); + + expect(result).toEqual(['fr']); + }); + + it('should return empty array when no language templates exist', () => { + const cascade: CascadeItem[] = [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + ]; + + const result = getRemainingLanguages(cascade); + + expect(result).toEqual([]); + }); +}); + +describe('updateCascadeGroupOverrides', () => { + it('should update accessible group with remaining formats', () => { + const cascadeGroupOverrides = [ + { + name: 'accessible' as const, + accessibleFormat: ['q4', 'x0', 'x1'] as LetterType[], + }, + { name: 'standard' as const }, + ]; + + const updatedCascade: CascadeItem[] = [ + { + cascadeGroups: ['accessible'], + channel: 'LETTER', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', accessibleFormat: 'q4' }, + ], + }, + ]; + + const result = updateCascadeGroupOverrides( + cascadeGroupOverrides, + updatedCascade + ); + + expect(result).toEqual([ + { name: 'accessible', accessibleFormat: ['q4'] }, + { name: 'standard' }, + ]); + }); + + it('should update translations group with remaining languages', () => { + const cascadeGroupOverrides = [ + { + name: 'translations' as const, + language: ['fr', 'es', 'de'] as Language[], + }, + { name: 'standard' as const }, + ]; + + const updatedCascade: CascadeItem[] = [ + { + cascadeGroups: ['translations'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: null, + conditionalTemplates: [ + { templateId: 'template-1', language: 'fr' }, + { templateId: 'template-2', language: 'es' }, + ], + }, + ]; + + const result = updateCascadeGroupOverrides( + cascadeGroupOverrides, + updatedCascade + ); + + expect(result.length).toBe(2); + expect(result[0]).toMatchObject({ + name: 'translations', + language: ['fr', 'es'], + }); + + expect(result[1]).toEqual({ name: 'standard' }); + }); + + it('should remove accessible group when no formats remain', () => { + const cascadeGroupOverrides = [ + { + name: 'accessible' as const, + accessibleFormat: ['q4'] as LetterType[], + }, + { name: 'standard' as const }, + ]; + + const updatedCascade: CascadeItem[] = [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + ]; + + const result = updateCascadeGroupOverrides( + cascadeGroupOverrides, + updatedCascade + ); + + expect(result).toEqual([{ name: 'standard' }]); + }); + + it('should remove translations group when no languages remain', () => { + const cascadeGroupOverrides = [ + { name: 'translations' as const, language: ['fr'] as Language[] }, + { name: 'standard' as const }, + ]; + + const updatedCascade: CascadeItem[] = [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + ]; + + const result = updateCascadeGroupOverrides( + cascadeGroupOverrides, + updatedCascade + ); + + expect(result).toEqual([{ name: 'standard' }]); + }); + + it('should keep standard group unchanged', () => { + const cascadeGroupOverrides = [{ name: 'standard' as const }]; + + const updatedCascade: CascadeItem[] = [ + { + cascadeGroups: ['standard'], + channel: 'EMAIL', + channelType: 'primary', + defaultTemplateId: 'template-1', + }, + ]; + + const result = updateCascadeGroupOverrides( + cascadeGroupOverrides, + updatedCascade + ); + + expect(result).toEqual([{ name: 'standard' }]); + }); +}); diff --git a/frontend/src/utils/get-message-plan-template-ids.ts b/frontend/src/utils/get-message-plan-template-ids.ts deleted file mode 100644 index a83ce4fa5..000000000 --- a/frontend/src/utils/get-message-plan-template-ids.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RoutingConfig } from 'nhs-notify-backend-client'; - -/** - * Extracts all template IDs from a RoutingConfig - */ -export function getMessagePlanTemplateIds( - messagePlan: RoutingConfig -): Set { - const templateIds = new Set(); - - for (const cascadeItem of messagePlan.cascade) { - if (cascadeItem.defaultTemplateId) - templateIds.add(cascadeItem.defaultTemplateId); - if (cascadeItem.conditionalTemplates) { - for (const conditionalTemplate of cascadeItem.conditionalTemplates) { - if (conditionalTemplate.templateId) - templateIds.add(conditionalTemplate.templateId); - } - } - } - - return templateIds; -} diff --git a/frontend/src/utils/message-plans.ts b/frontend/src/utils/message-plans.ts index 4bed922d0..a3dc63b69 100644 --- a/frontend/src/utils/message-plans.ts +++ b/frontend/src/utils/message-plans.ts @@ -5,10 +5,12 @@ import { CreateRoutingConfig, RoutingConfig, RoutingConfigStatusActive, - TemplateDto, UpdateRoutingConfig, } from 'nhs-notify-backend-client'; -import { getMessagePlanTemplateIds } from './get-message-plan-template-ids'; +import { + getMessagePlanTemplateIds, + type MessagePlanTemplates, +} from './routing-utils'; import { getSessionServer } from './amplify-utils'; import { logger } from 'nhs-notify-web-template-management-utils/logger'; import { getTemplate } from './form-actions'; @@ -149,8 +151,9 @@ export async function updateRoutingConfig( return result.data; } -export type MessagePlanTemplates = Record; - +/** + * Fetches all templates referenced in a message plan + */ export async function getMessagePlanTemplates( messagePlan: RoutingConfig ): Promise { @@ -161,6 +164,9 @@ export async function getMessagePlanTemplates( return getTemplatesByIds([...templateIds]); } +/** + * Fetches templates by their IDs and returns a map of template ID to template object + */ export async function getTemplatesByIds( templateIds: string[] ): Promise { diff --git a/frontend/src/utils/routing-utils.ts b/frontend/src/utils/routing-utils.ts new file mode 100644 index 000000000..32ed17c39 --- /dev/null +++ b/frontend/src/utils/routing-utils.ts @@ -0,0 +1,163 @@ +import { + CascadeGroup, + CascadeItem, + ConditionalTemplateAccessible, + ConditionalTemplateLanguage, + Language, + LetterType, + RoutingConfig, + TemplateDto, +} from 'nhs-notify-backend-client'; + +export type ConditionalTemplate = + | ConditionalTemplateAccessible + | ConditionalTemplateLanguage; + +export type MessagePlanTemplates = Record; + +/** + * Extracts all template IDs from a RoutingConfig + */ +export function getMessagePlanTemplateIds( + messagePlan: RoutingConfig +): Set { + const templateIds = new Set(); + + for (const cascadeItem of messagePlan.cascade) { + if (cascadeItem.defaultTemplateId) + templateIds.add(cascadeItem.defaultTemplateId); + if (cascadeItem.conditionalTemplates) { + for (const conditionalTemplate of cascadeItem.conditionalTemplates) { + if (conditionalTemplate.templateId) + templateIds.add(conditionalTemplate.templateId); + } + } + } + + return templateIds; +} + +/** + * Checks if a template ID should be removed based on the removal list + */ +export function shouldRemoveTemplate( + templateId: string | null | undefined, + templateIdsToRemove: string[] +): boolean { + return !!templateId && templateIdsToRemove.includes(templateId); +} + +/** + * Filters out conditional templates whose IDs match the removal list + */ +export function removeTemplatesFromConditionalTemplates( + conditionalTemplates: ConditionalTemplate[], + templateIdsToRemove: string[] +): ConditionalTemplate[] { + return conditionalTemplates.filter( + (template) => + !shouldRemoveTemplate(template.templateId, templateIdsToRemove) + ); +} + +/** + * Removes templates from a cascade item by ID, updating both default and conditional templates + */ +export function removeTemplatesFromCascadeItem( + cascadeItem: CascadeItem, + templateIdsToRemove: string[] +): CascadeItem { + const updatedCascadeItem: CascadeItem = { ...cascadeItem }; + + const defaultTemplateId = shouldRemoveTemplate( + cascadeItem.defaultTemplateId, + templateIdsToRemove + ) + ? null + : cascadeItem.defaultTemplateId; + + updatedCascadeItem.defaultTemplateId = defaultTemplateId ?? null; + + if (cascadeItem.conditionalTemplates) { + updatedCascadeItem.conditionalTemplates = + removeTemplatesFromConditionalTemplates( + cascadeItem.conditionalTemplates, + templateIdsToRemove + ); + } + + return updatedCascadeItem; +} + +/** + * Collects all remaining accessible format types from the cascade + */ +export function getRemainingAccessibleFormats( + cascade: CascadeItem[] +): LetterType[] { + const formats = new Set(); + + for (const item of cascade) { + if (!item.conditionalTemplates) continue; + for (const template of item.conditionalTemplates) { + if ('accessibleFormat' in template && template.templateId) { + formats.add(template.accessibleFormat); + } + } + } + return [...formats]; +} + +/** + * Collects all remaining language types from the cascade + */ +export function getRemainingLanguages(cascade: CascadeItem[]): Language[] { + const languages = new Set(); + + for (const item of cascade) { + if (!item.conditionalTemplates) continue; + for (const template of item.conditionalTemplates) { + if ('language' in template && template.templateId) { + languages.add(template.language); + } + } + } + return [...languages]; +} + +/** + * Updates cascadeGroupOverrides by removing groups with no templates + * or updating their arrays to reflect remaining templates + */ +export function updateCascadeGroupOverrides( + cascadeGroupOverrides: CascadeGroup[], + updatedCascade: CascadeItem[] +): CascadeGroup[] { + return cascadeGroupOverrides + .map((group): CascadeGroup => { + if ('accessibleFormat' in group) { + return { + ...group, + accessibleFormat: getRemainingAccessibleFormats(updatedCascade), + }; + } + + if ('language' in group) { + return { + ...group, + language: getRemainingLanguages(updatedCascade), + }; + } + + return group; + }) + .filter((group) => { + if ('accessibleFormat' in group) { + return group.accessibleFormat.length > 0; + } + if ('language' in group) { + return group.language.length > 0; + } + return true; + }); +} From 6b1f8776e922c562ed44b76f92d587735b0882c3 Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 11:52:58 +0000 Subject: [PATCH 02/23] CCM-11494 Update interpolate function --- frontend/src/__tests__/utils/interpolate.test.ts | 12 ++++++++++++ frontend/src/utils/interpolate.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/__tests__/utils/interpolate.test.ts b/frontend/src/__tests__/utils/interpolate.test.ts index 4653d4a9c..584f890e0 100644 --- a/frontend/src/__tests__/utils/interpolate.test.ts +++ b/frontend/src/__tests__/utils/interpolate.test.ts @@ -5,6 +5,10 @@ describe('interpolate', () => { expect(interpolate('', { name: 'Test' })).toBe(''); }); + it('uses default empty object when no variables provided', () => { + expect(interpolate('Hello {{name}}!')).toBe('Hello !'); + }); + it('returns the same string if no placeholders are present', () => { expect(interpolate('No interpolation needed.', {})).toBe( 'No interpolation needed.' @@ -24,6 +28,14 @@ describe('interpolate', () => { it('replaces plural based on numeric value', () => { expect(interpolate('{{count|item|items}}', { count: 1 })).toBe('item'); expect(interpolate('{{count|item|items}}', { count: 3 })).toBe('items'); + expect(interpolate('{{count|item|items}}', { count: 0 })).toBe('items'); + }); + + it('uses empty string in singular position', () => { + expect(interpolate('Remove{{count|| all}}', { count: 1 })).toBe('Remove'); + expect(interpolate('Remove{{count|| all}}', { count: 2 })).toBe( + 'Remove all' + ); }); it('falls back to plural if variable is not a number', () => { diff --git a/frontend/src/utils/interpolate.ts b/frontend/src/utils/interpolate.ts index ce44db815..3685878c1 100644 --- a/frontend/src/utils/interpolate.ts +++ b/frontend/src/utils/interpolate.ts @@ -13,7 +13,7 @@ */ // eslint-disable-next-line security/detect-unsafe-regex, sonarjs/slow-regex -const interpolationPattern = /{{([^|}]+)(?:\|([^|}]+)\|([^}]+))?}}/g; +const interpolationPattern = /{{([^|}]+)(?:\|([^|}]*)\|([^}]*))?}}/g; export function interpolate( template: string, From a57c9627a3dcdb21b9ea69d877c78b4f2eb6b934 Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 16:06:42 +0000 Subject: [PATCH 03/23] CCM-11494 Update arrow icons --- frontend/public/lib/assets/icons/icon-arrow-down.svg | 2 +- frontend/public/lib/assets/icons/icon-arrow-left.svg | 2 +- frontend/public/lib/assets/icons/icon-arrow-right.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/public/lib/assets/icons/icon-arrow-down.svg b/frontend/public/lib/assets/icons/icon-arrow-down.svg index 40ab19280..36c451025 100644 --- a/frontend/public/lib/assets/icons/icon-arrow-down.svg +++ b/frontend/public/lib/assets/icons/icon-arrow-down.svg @@ -8,7 +8,7 @@ > diff --git a/frontend/public/lib/assets/icons/icon-arrow-left.svg b/frontend/public/lib/assets/icons/icon-arrow-left.svg index d370ebe25..152a954f1 100644 --- a/frontend/public/lib/assets/icons/icon-arrow-left.svg +++ b/frontend/public/lib/assets/icons/icon-arrow-left.svg @@ -1,6 +1,6 @@ diff --git a/frontend/public/lib/assets/icons/icon-arrow-right.svg b/frontend/public/lib/assets/icons/icon-arrow-right.svg index ee4cd5a6c..e5ef36502 100644 --- a/frontend/public/lib/assets/icons/icon-arrow-right.svg +++ b/frontend/public/lib/assets/icons/icon-arrow-right.svg @@ -1,6 +1,6 @@ From 571a970041c6ec1d2eca098d37e1b140848ef10b Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 16:09:13 +0000 Subject: [PATCH 04/23] CCM-11494 URLs for choosing templates --- utils/utils/src/__tests__/enum.test.ts | 16 +++++++++++++++ utils/utils/src/enum.ts | 27 +++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/utils/utils/src/__tests__/enum.test.ts b/utils/utils/src/__tests__/enum.test.ts index 36c4f92d9..e443b41d9 100644 --- a/utils/utils/src/__tests__/enum.test.ts +++ b/utils/utils/src/__tests__/enum.test.ts @@ -241,6 +241,22 @@ describe('messagePlanChooseTemplateUrl', () => { ] as const)('should map %s to "%s"', (type, expected) => { expect(messagePlanChooseTemplateUrl(type)).toBe(expected); }); + + describe('conditional letter templates', () => { + test.each([ + ['q4', 'choose-large-print-letter-template'], + ['x0', 'choose-audio-cd-letter-template'], + ['x1', 'choose-braille-letter-template'], + ['language', 'choose-other-language-letter-template'], + ] as const)( + 'should map LETTER with conditionalType %s to "%s"', + (conditionalType, expected) => { + expect(messagePlanChooseTemplateUrl('LETTER', conditionalType)).toBe( + expected + ); + } + ); + }); }); describe('previewTemplatePages', () => { diff --git a/utils/utils/src/enum.ts b/utils/utils/src/enum.ts index b182c6e96..c76228128 100644 --- a/utils/utils/src/enum.ts +++ b/utils/utils/src/enum.ts @@ -200,10 +200,27 @@ export const previewTemplatePages = (type: TemplateType) => export const previewSubmittedTemplatePages = (type: TemplateType) => `preview-submitted-${templateTypeToUrlTextMappings(type)}-template`; -export const messagePlanChooseTemplateUrl = (type: TemplateType) => - type === 'LETTER' - ? 'choose-standard-english-letter-template' - : `choose-${templateTypeToUrlTextMappings(type)}-template`; +const conditionalLetterTemplateUrlSegments: Record< + LetterType | 'language', + string +> = { + q4: 'large-print-letter', + x0: 'audio-cd-letter', + x1: 'braille-letter', + language: 'other-language-letter', +}; + +export const messagePlanChooseTemplateUrl = ( + type: TemplateType, + conditionalType?: LetterType | 'language' +) => { + const urlSegment = + type === 'LETTER' && conditionalType + ? conditionalLetterTemplateUrlSegments[conditionalType] + : cascadeTemplateTypeToUrlTextMappings(type); + + return `choose-${urlSegment}-template`; +}; const templateStatusCopyAction = (status: TemplateStatus) => ( @@ -322,7 +339,7 @@ export const channelDisplayMappings = (channel: Channel) => { NHSAPP: 'NHS App', SMS: 'Text message (SMS)', EMAIL: 'Email', - LETTER: 'Letter', + LETTER: 'Standard English letter', }; return map[channel]; }; From 4df03bbfcd1ee162e23b4e597ade57a6533c9f73 Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 16:24:06 +0000 Subject: [PATCH 05/23] CCM-11494 ChannelTemplate supports accessible formats and translated languages --- .../MessagePlanChannelTemplate.test.tsx | 254 ++++++++++++- .../MessagePlanChannelTemplate.test.tsx.snap | 337 ++++++++++++++++-- .../MessagePlanChannelTemplate.tsx | 184 ++++++++-- 3 files changed, 697 insertions(+), 78 deletions(-) diff --git a/frontend/src/__tests__/components/molecules/MessagePlanChannelTemplate.test.tsx b/frontend/src/__tests__/components/molecules/MessagePlanChannelTemplate.test.tsx index 8f6cfad93..d39449b7e 100644 --- a/frontend/src/__tests__/components/molecules/MessagePlanChannelTemplate.test.tsx +++ b/frontend/src/__tests__/components/molecules/MessagePlanChannelTemplate.test.tsx @@ -1,5 +1,9 @@ import { render, screen } from '@testing-library/react'; -import { MessagePlanChannelTemplate } from '@molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate'; +import { + MessagePlanChannelTemplate, + MessagePlanAccessibleFormatTemplate, + MessagePlanLanguageTemplate, +} from '@molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate'; import type { TemplateDto } from 'nhs-notify-backend-client'; describe('MessagePlanChannelTemplate', () => { @@ -31,7 +35,10 @@ describe('MessagePlanChannelTemplate', () => { it('should display the heading with the "(optional)" suffix', () => { expect( - screen.getByRole('heading', { level: 3, name: 'Letter (optional)' }) + screen.getByRole('heading', { + level: 3, + name: 'Standard English letter (optional)', + }) ).toBeInTheDocument(); }); }); @@ -61,7 +68,7 @@ describe('MessagePlanChannelTemplate', () => { screen.queryByRole('link', { name: 'Change NHS App template' }) ).not.toBeInTheDocument(); expect( - screen.queryByRole('link', { name: 'Remove NHS App template' }) + screen.queryByRole('button', { name: 'Remove NHS App template' }) ).not.toBeInTheDocument(); }); }); @@ -105,14 +112,15 @@ describe('MessagePlanChannelTemplate', () => { expect(removeButton).toBeInTheDocument(); expect(form).toBeInTheDocument(); - const channelInput = form?.querySelector('input[name="channel"]'); const routingConfigIdInput = form?.querySelector( 'input[name="routingConfigId"]' ); - expect(channelInput).toHaveAttribute('type', 'hidden'); - expect(channelInput).toHaveAttribute('value', 'SMS'); + const templateIdInput = form?.querySelector('input[name="templateId"]'); + expect(routingConfigIdInput).toHaveAttribute('type', 'hidden'); expect(routingConfigIdInput).toHaveAttribute('value', routingConfigId); + expect(templateIdInput).toHaveAttribute('type', 'hidden'); + expect(templateIdInput).toHaveAttribute('value', testTemplate.id); }); it('should not display the "Choose template" link', () => { @@ -157,3 +165,237 @@ describe('MessagePlanChannelTemplate', () => { } ); }); + +describe('MessagePlanAccessibleFormatTemplate', () => { + const routingConfigId = 'test-routing-config-id'; + + it('should display the accessible format heading', () => { + render( + + ); + + expect( + screen.getByRole('heading', { + level: 3, + name: 'Large print letter (optional)', + }) + ).toBeInTheDocument(); + }); + + describe('when no template is selected', () => { + beforeEach(() => { + render( + + ); + }); + + it('should not display template name', () => { + expect(screen.queryByTestId('template-names')).not.toBeInTheDocument(); + }); + + it('should show the "Choose template" link (singular) with accessible name and href', () => { + const link = screen.getByRole('link', { + name: 'Choose Large print letter template', + }); + expect(link).toHaveAttribute( + 'href', + `/message-plans/choose-large-print-letter-template/${routingConfigId}` + ); + }); + }); + + describe('when a template has been selected', () => { + const testTemplate = { + id: 'test-large-print-id', + name: 'Large print letter template', + } as TemplateDto; + + beforeEach(() => { + render( + + ); + }); + + it('should display the selected template name', () => { + expect(screen.getByText(testTemplate.name)).toBeInTheDocument(); + }); + + it('should display the "Change template" link', () => { + const link = screen.getByRole('link', { + name: 'Change Large print letter template', + }); + expect(link).toHaveAttribute( + 'href', + `/message-plans/choose-large-print-letter-template/${routingConfigId}` + ); + }); + + it('should display the "Remove template" button', () => { + const removeButton = screen.getByRole('button', { + name: 'Remove Large print letter template', + }); + expect(removeButton).toBeInTheDocument(); + }); + + it.each(['q4'] as const)( + 'should match snapshot for empty state for letter type (%s)', + (accessibleFormat) => { + const { container } = render( + + ); + + expect(container).toMatchSnapshot(); + } + ); + + it.each(['q4'] as const)( + 'should match snapshot for template selected state for letter type (%s)', + (accessibleFormat) => { + const { container } = render( + + ); + + expect(container).toMatchSnapshot(); + } + ); + }); +}); + +describe('MessagePlanLanguageTemplate', () => { + const routingConfigId = 'test-routing-config-id'; + + it('should display the language templates heading', () => { + render( + + ); + + expect( + screen.getByRole('heading', { + level: 3, + name: 'Other language letters (optional)', + }) + ).toBeInTheDocument(); + }); + + describe('when no templates are selected', () => { + beforeEach(() => { + render( + + ); + }); + + it('should show the "Choose templates" link (plural) with accessible name and href', () => { + const link = screen.getByRole('link', { + name: 'Choose Other language letters templates', + }); + expect(link).toHaveAttribute( + 'href', + `/message-plans/choose-other-language-letter-template/${routingConfigId}` + ); + }); + }); + + describe('when multiple templates are selected', () => { + const testTemplates = [ + { + id: 'welsh-template-id', + name: 'Welsh letter template', + } as TemplateDto, + { + id: 'polish-template-id', + name: 'Polish letter template', + } as TemplateDto, + ]; + + beforeEach(() => { + render( + + ); + }); + + it('should display all selected template names', () => { + expect(screen.getByText('Welsh letter template')).toBeInTheDocument(); + expect(screen.getByText('Polish letter template')).toBeInTheDocument(); + }); + + it('should display the "Change templates" link (plural)', () => { + const link = screen.getByRole('link', { + name: 'Change Other language letters templates', + }); + expect(link).toHaveAttribute( + 'href', + `/message-plans/choose-other-language-letter-template/${routingConfigId}` + ); + }); + + it('should display the "Remove all templates" button with all template IDs', () => { + const removeButton = screen.getByRole('button', { + name: 'Remove all Other language letters templates', + }); + expect(removeButton).toBeInTheDocument(); + + const form = removeButton.closest('form'); + const templateIdInputs = form?.querySelectorAll( + 'input[name="templateId"]' + ); + + expect(templateIdInputs).toHaveLength(2); + expect(templateIdInputs?.[0]).toHaveAttribute( + 'value', + 'welsh-template-id' + ); + expect(templateIdInputs?.[1]).toHaveAttribute( + 'value', + 'polish-template-id' + ); + }); + + it('should match snapshot for empty state', () => { + const { container } = render( + + ); + + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with multiple templates selected', () => { + const { container } = render( + + ); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanChannelTemplate.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanChannelTemplate.test.tsx.snap index b78daea5d..e77ec3d65 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanChannelTemplate.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanChannelTemplate.test.tsx.snap @@ -1,5 +1,121 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`MessagePlanAccessibleFormatTemplate when a template has been selected should match snapshot for empty state for letter type (q4) 1`] = ` +
+
+
+

+ Large print letter (optional) +

+ +
+
+
+`; + +exports[`MessagePlanAccessibleFormatTemplate when a template has been selected should match snapshot for template selected state for letter type (q4) 1`] = ` +
+
+
+

+ Large print letter (optional) +

+
+

+ Large print letter template +

+
+ +
+
+
+`; + exports[`MessagePlanChannelTemplate should match snapshot for empty state (EMAIL) 1`] = `
- Letter + Standard English letter
    - Letter + Standard English letter template @@ -165,12 +281,17 @@ exports[`MessagePlanChannelTemplate should match snapshot for selected template > Email -

    - Covid 65+ reminder v1 -

    +

    + Covid 65+ reminder v1 +

    +
    @@ -193,14 +314,14 @@ exports[`MessagePlanChannelTemplate should match snapshot for selected template
    @@ -257,7 +383,7 @@ exports[`MessagePlanChannelTemplate should match snapshot for selected template - Letter + Standard English letter template @@ -266,14 +392,14 @@ exports[`MessagePlanChannelTemplate should match snapshot for selected template
  • + +
  • +
+ + + +`; diff --git a/frontend/src/components/molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate.tsx b/frontend/src/components/molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate.tsx index 99e54dc81..eed7395e8 100644 --- a/frontend/src/components/molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate.tsx +++ b/frontend/src/components/molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate.tsx @@ -1,5 +1,10 @@ import Link from 'next/link'; -import { Channel, RoutingConfig, TemplateDto } from 'nhs-notify-backend-client'; +import { + Channel, + LetterType, + RoutingConfig, + TemplateDto, +} from 'nhs-notify-backend-client'; import { channelDisplayMappings, channelToTemplateType, @@ -7,94 +12,124 @@ import { } from 'nhs-notify-web-template-management-utils'; import classNames from 'classnames'; import { removeTemplateFromMessagePlan } from '@app/message-plans/choose-templates/[routingConfigId]/actions'; +import { interpolate } from '@utils/interpolate'; import styles from '@molecules/MessagePlanChannelTemplate/MessagePlanChannelTemplate.module.scss'; import copy from '@content/content'; const { messagePlanChannelTemplate: content } = copy.components; -export function MessagePlanChannelTemplate({ - channel, - template, +function MessagePlanChannelTemplateBase({ + channelTemplateType, + required, + templates, routingConfigId, - required = true, + chooseTemplateUrl, + removeTemplateAction, + testIdSuffix, + multipleTemplates = false, }: { - channel: Channel; - routingConfigId: RoutingConfig['id']; - template?: TemplateDto; - required?: boolean; + channelTemplateType: string; + required: boolean; + templates: TemplateDto[]; + routingConfigId: string; + chooseTemplateUrl: string; + removeTemplateAction: (formData: FormData) => Promise; + testIdSuffix: string; + multipleTemplates?: boolean; }) { - const channelDisplayText = - channelDisplayMappings(channel) + (required ? '' : ` ${content.optional}`); + const templateCount = templates.length; + const hasTemplates = templateCount > 0; return (
-

{channelDisplayText}

+

{`${channelTemplateType}${required ? '' : ' (optional)'}`}

- {template && ( -

- {template.name} -

+ {hasTemplates && ( +
+ {templates.map((template) => ( +

+ {template.name} +

+ ))} +
)}
    - {!template && ( + {!hasTemplates && (
  • {content.templateLinks.choose} - {channelDisplayText} + {channelTemplateType} {' '} - {content.templateLinks.template} + {interpolate(content.templateLinks.templateWord, { + templateCount: multipleTemplates ? 2 : 1, + })}
  • )} - {template && ( + {hasTemplates && ( <>
  • {content.templateLinks.change} - {channelDisplayText} + {channelTemplateType} {' '} - {content.templateLinks.template} + {interpolate(content.templateLinks.templateWord, { + templateCount, + })}
  • +
  • - + {templates.map((template) => ( + + ))}
  • @@ -105,3 +140,82 @@ export function MessagePlanChannelTemplate({
); } + +export function MessagePlanChannelTemplate({ + channel, + template, + routingConfigId, + required = true, +}: { + channel: Channel; + routingConfigId: RoutingConfig['id']; + template?: TemplateDto; + required?: boolean; +}) { + const chooseTemplateUrl = `/message-plans/${messagePlanChooseTemplateUrl(channelToTemplateType(channel))}/${routingConfigId}`; + + return ( + + ); +} + +export function MessagePlanAccessibleFormatTemplate({ + accessibleFormat, + template, + routingConfigId, +}: { + accessibleFormat: LetterType; + template?: TemplateDto; + routingConfigId: string; +}) { + const chooseTemplateUrl = `/message-plans/${messagePlanChooseTemplateUrl('LETTER', accessibleFormat)}/${routingConfigId}`; + + return ( + + ); +} + +export function MessagePlanLanguageTemplate({ + selectedTemplates, + routingConfigId, +}: { + selectedTemplates: TemplateDto[]; + routingConfigId: string; +}) { + const chooseTemplateUrl = `/message-plans/${messagePlanChooseTemplateUrl('LETTER', 'language')}/${routingConfigId}`; + + return ( + + ); +} From f07f45f96960657d26e9549f6d2390b50011b133 Mon Sep 17 00:00:00 2001 From: Nicki Derrick Date: Tue, 25 Nov 2025 16:25:18 +0000 Subject: [PATCH 06/23] CCM-11494 Add additional class to fallback conditions --- .../MessagePlanFallbackConditions.test.tsx.snap | 8 ++++---- .../MessagePlanFallbackConditions.tsx | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanFallbackConditions.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanFallbackConditions.test.tsx.snap index 7fb4d29e3..4bcfe5d88 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanFallbackConditions.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanFallbackConditions.test.tsx.snap @@ -3,7 +3,7 @@ exports[`MessagePlanFallbackConditions should match snapshot for EMAIL at index 1 1`] = `
  • Date: Tue, 25 Nov 2025 16:32:45 +0000 Subject: [PATCH 07/23] CCm-11494 MessagePlanBlock supports alternative letter formats --- .../molecules/MessagePlanBlock.test.tsx | 113 ++++- .../MessagePlanBlock.test.tsx.snap | 412 ++++++++++++++++-- .../MessagePlanBlock/MessagePlanBlock.tsx | 17 +- 3 files changed, 497 insertions(+), 45 deletions(-) diff --git a/frontend/src/__tests__/components/molecules/MessagePlanBlock.test.tsx b/frontend/src/__tests__/components/molecules/MessagePlanBlock.test.tsx index f6e5b643a..aa5930621 100644 --- a/frontend/src/__tests__/components/molecules/MessagePlanBlock.test.tsx +++ b/frontend/src/__tests__/components/molecules/MessagePlanBlock.test.tsx @@ -3,7 +3,8 @@ import { render, screen } from '@testing-library/react'; import { MessagePlanBlock } from '@molecules/MessagePlanBlock/MessagePlanBlock'; import type { CascadeItem, Channel } from 'nhs-notify-backend-client'; import type { TemplateDto } from 'nhs-notify-backend-client'; -import { EMAIL_TEMPLATE } from '@testhelpers/helpers'; +import { EMAIL_TEMPLATE, LETTER_TEMPLATE } from '@testhelpers/helpers'; +import { MessagePlanTemplates } from '@utils/routing-utils'; function buildCascadeItem(channel: Channel): CascadeItem { return { @@ -19,6 +20,8 @@ const mockTemplate: TemplateDto = { name: 'Test email template', }; +const emptyConditionalTemplates: MessagePlanTemplates = {}; + describe('MessagePlanBlock', () => { it('should render the step number and the heading for the first cascade item', () => { const channelItem = buildCascadeItem('EMAIL'); @@ -28,6 +31,7 @@ describe('MessagePlanBlock', () => { index={0} channelItem={channelItem} routingConfigId='test-routing-config-id' + conditionalTemplates={emptyConditionalTemplates} /> ); @@ -47,6 +51,7 @@ describe('MessagePlanBlock', () => { index={2} channelItem={channelItem} routingConfigId='test-routing-config-id' + conditionalTemplates={emptyConditionalTemplates} /> ); @@ -65,6 +70,7 @@ describe('MessagePlanBlock', () => { index={0} channelItem={channelItem} routingConfigId='test-routing-config-id' + conditionalTemplates={emptyConditionalTemplates} /> ); @@ -81,8 +87,9 @@ describe('MessagePlanBlock', () => { ); expect(screen.getByText('Test email template')).toBeInTheDocument(); @@ -95,8 +102,9 @@ describe('MessagePlanBlock', () => { ); @@ -121,6 +129,7 @@ describe('MessagePlanBlock', () => { index={0} channelItem={channelItem} routingConfigId='test-routing-config-id' + conditionalTemplates={emptyConditionalTemplates} /> ); @@ -133,13 +142,105 @@ describe('MessagePlanBlock', () => { }) ).not.toBeInTheDocument(); expect( - screen.queryByRole('link', { + screen.queryByRole('button', { name: 'Remove Text message (SMS) template', }) ).not.toBeInTheDocument(); }); }); + describe('when channel is LETTER', () => { + it('should render conditional templates section', () => { + const channelItem = buildCascadeItem('LETTER'); + + render( + + ); + + expect( + screen.getByTestId('message-plan-conditional-templates') + ).toBeInTheDocument(); + }); + + it('should render accessible format templates', () => { + const channelItem = buildCascadeItem('LETTER'); + + render( + + ); + + expect( + screen.getByRole('heading', { + level: 3, + name: 'Large print letter (optional)', + }) + ).toBeInTheDocument(); + }); + + it('should render language templates section', () => { + const channelItem = buildCascadeItem('LETTER'); + + render( + + ); + + expect( + screen.getByRole('heading', { + level: 3, + name: 'Other language letters (optional)', + }) + ).toBeInTheDocument(); + }); + + it('should display conditional template names when provided', () => { + const largePrintTemplate: TemplateDto = { + ...LETTER_TEMPLATE, + id: 'large-print-id', + name: 'Large print template', + }; + + const channelItem: CascadeItem = { + ...buildCascadeItem('LETTER'), + conditionalTemplates: [ + { + accessibleFormat: 'q4', + templateId: 'large-print-id', + }, + ], + }; + + const conditionalTemplates: MessagePlanTemplates = { + 'large-print-id': largePrintTemplate, + }; + + render( + + ); + + expect(screen.getByText('Large print template')).toBeInTheDocument(); + }); + }); + describe.each(['NHSAPP', 'EMAIL', 'SMS', 'LETTER'] as const)( 'for channel %s with template', (channel) => { @@ -149,8 +250,9 @@ describe('MessagePlanBlock', () => { ); expect(asFragment()).toMatchSnapshot(); @@ -168,6 +270,7 @@ describe('MessagePlanBlock', () => { index={0} channelItem={channelItem} routingConfigId='test-routing-config-id' + conditionalTemplates={emptyConditionalTemplates} /> ); expect(asFragment()).toMatchSnapshot(); diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanBlock.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanBlock.test.tsx.snap index 2e6ad30b9..439a24276 100644 --- a/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanBlock.test.tsx.snap +++ b/frontend/src/__tests__/components/molecules/__snapshots__/MessagePlanBlock.test.tsx.snap @@ -81,12 +81,17 @@ exports[`MessagePlanBlock for channel EMAIL with template should match snapshot > Email -

    - Test email template -

    +

    + Test email template +

    +
      @@ -108,14 +113,14 @@ exports[`MessagePlanBlock for channel EMAIL with template should match snapshot
  • +
      +
    • + +
      + + + Conditions for accessible and language letters + + +
      +
        +
      • + + The relevant accessible or language letter will be sent instead of the standard English letter if, both: +
          +
        • + the recipient has requested an accessible or language letter in PDS +
        • +
        • + you've included the relevant template in this message plan +
        • +
        +
      • +
      +
      +
      +
    • +
    • +
      +
      +

      + Large print letter (optional) +

      + +
      +
      +
    • +
    • +
      +
      +

      + Other language letters (optional) +

      + +
      +
      +
    • +
    `; @@ -219,14 +383,19 @@ exports[`MessagePlanBlock for channel LETTER with template should match snapshot

    - Letter + Standard English letter

    -

    - Test email template -

    +

    + Test email template +

    +
      @@ -240,7 +409,7 @@ exports[`MessagePlanBlock for channel LETTER with template should match snapshot - Letter + Standard English letter template @@ -248,14 +417,14 @@ exports[`MessagePlanBlock for channel LETTER with template should match snapshot
    • @@ -276,6 +445,165 @@ exports[`MessagePlanBlock for channel LETTER with template should match snapshot
    +
      +
    • + +
      + + + Conditions for accessible and language letters + + +
      +
        +
      • + + The relevant accessible or language letter will be sent instead of the standard English letter if, both: +
          +
        • + the recipient has requested an accessible or language letter in PDS +
        • +
        • + you've included the relevant template in this message plan +
        • +
        +
      • +
      +
      +
      +
    • +
    • +
      +
      +

      + Large print letter (optional) +

      + +
      +
      +
    • +
    • +
      +
      +

      + Other language letters (optional) +

      + +
      +
      +
    • +
    `; @@ -361,12 +689,17 @@ exports[`MessagePlanBlock for channel NHSAPP with template should match snapshot > NHS App -

    - Test email template -

    +

    + Test email template +

    +