From 1be27404fae5974a765cf28ea98a34a5e4f8b8e6 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Tue, 1 Sep 2026 16:40:02 +0200 Subject: [PATCH] fix(email): derive preview sample data from each template's variables The preview modal's hardcoded sampleData had drifted from the templates' declared variables arrays: it carried `password` and `expiration_date` and no `host_name` at all, so {{host_name}}, {{gallery_password}} and {{expiry_date}} rendered as literal placeholders in the gallery_created preview while event_name/event_date/gallery_link substituted fine. Derive the key set from the template's own `variables` instead, so nothing can be missing again. editedTemplate already carries the array at the call site, so no plumbing was needed. A small module-level lookup keeps sensible shapes for the ~11 variables where shape matters (dates look like dates, links like URLs), with a readable [name] fallback for anything uncurated -- curating all ~60 distinct variable names across the ~32 template seeds would just recreate the drift trap. Preview-only; real sent mail was never affected. Refs testplan REPORT.md #17 (Part 3, J.04). --- frontend/src/pages/admin/EmailConfigPage.tsx | 47 ++++++++++++---- .../__tests__/emailPreviewSampleData.test.ts | 56 +++++++++++++++++++ 2 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 frontend/src/pages/admin/__tests__/emailPreviewSampleData.test.ts diff --git a/frontend/src/pages/admin/EmailConfigPage.tsx b/frontend/src/pages/admin/EmailConfigPage.tsx index 670c4bdd..88ba8b7c 100644 --- a/frontend/src/pages/admin/EmailConfigPage.tsx +++ b/frontend/src/pages/admin/EmailConfigPage.tsx @@ -68,6 +68,39 @@ const CORE_SUBCATEGORY_ORDER: readonly string[] = [ 'system', ] as const; +/** + * Realistic stand-ins for the variables whose *shape* matters in a + * preview — a date has to read like a date, a link like a link. This is + * deliberately not a full list of every variable every template declares; + * `buildPreviewSampleData` below covers the rest. + */ +const PREVIEW_SAMPLE_VALUES: Record = { + event_name: 'John & Jane Wedding', + event_date: 'December 25, 2024', + expiry_date: 'January 25, 2025', + gallery_link: 'https://photos.example.com/gallery/john-jane-wedding', + gallery_password: '••••••••', + password: '••••••••', + host_name: 'Jane Doe', + host_email: 'host@example.com', + admin_email: 'admin@example.com', + days_remaining: '30', + welcome_message: 'Thank you for celebrating our special day with us!', +}; + +/** + * Build the preview payload from the template's OWN declared `variables`, + * so the two can no longer drift apart. The previous hand-maintained key + * list had gone stale and left {{host_name}}, {{gallery_password}} and + * {{expiry_date}} rendering as raw tokens in the gallery_created preview. + * Variables without a curated value get a readable stand-in rather than an + * unsubstituted {{token}}. + */ +export const buildPreviewSampleData = (variables: string[] = []): Record => + Object.fromEntries( + variables.map((name) => [name, PREVIEW_SAMPLE_VALUES[name] ?? `[${name}]`]) + ); + const defaultTemplateKeys = [ { key: 'gallery_created', @@ -395,18 +428,8 @@ export const EmailConfigPage: React.FC = () => { const handlePreviewTemplate = async () => { if (!selectedTemplateKey || !editedTemplate) return; - // Generate sample data based on the template - const sampleData: Record = { - event_name: 'John & Jane Wedding', - event_date: 'December 25, 2024', - password: '••••••••', - gallery_link: 'https://photos.example.com/gallery/john-jane-wedding', - expiration_date: 'January 25, 2025', - welcome_message: 'Thank you for celebrating our special day with us!', - days_remaining: '30', - admin_email: 'admin@example.com', - host_email: 'host@example.com' - }; + // Sample data is derived from the template's declared variables + const sampleData = buildPreviewSampleData(editedTemplate.variables); try { const preview = await emailService.previewTemplate(selectedTemplateKey, sampleData, editingLang); diff --git a/frontend/src/pages/admin/__tests__/emailPreviewSampleData.test.ts b/frontend/src/pages/admin/__tests__/emailPreviewSampleData.test.ts new file mode 100644 index 00000000..e3361f1a --- /dev/null +++ b/frontend/src/pages/admin/__tests__/emailPreviewSampleData.test.ts @@ -0,0 +1,56 @@ +/** + * The template Preview modal substituted a hand-maintained sample-data object + * whose keys had drifted from the templates' declared `variables` (QA J.04): + * {{host_name}}, {{gallery_password}} and {{expiry_date}} rendered as literal + * tokens because the object still carried `password` / `expiration_date` and + * no `host_name` at all. The payload is now derived from `variables`, so the + * property that matters is coverage, not the contents of any one value. + */ +import { describe, it, expect } from 'vitest'; + +import { buildPreviewSampleData } from '../EmailConfigPage'; + +describe('buildPreviewSampleData', () => { + const galleryCreatedVariables = [ + 'host_name', + 'event_name', + 'event_date', + 'gallery_link', + 'gallery_password', + 'expiry_date', + ]; + + it('supplies a value for every variable the template declares', () => { + const sample = buildPreviewSampleData(galleryCreatedVariables); + + expect(Object.keys(sample).sort()).toEqual([...galleryCreatedVariables].sort()); + for (const name of galleryCreatedVariables) { + expect(sample[name]).toBeTruthy(); + } + }); + + it('never leaves a variable to render as a raw {{token}}', () => { + const sample = buildPreviewSampleData(['customer_name', 'invoice_number', 'total_amount']); + + for (const value of Object.values(sample)) { + expect(value).not.toMatch(/\{\{|\}\}/); + } + }); + + it('keeps date- and link-shaped variables looking like dates and links', () => { + const sample = buildPreviewSampleData(galleryCreatedVariables); + + expect(sample.event_date).toMatch(/\d{4}/); + expect(sample.expiry_date).toMatch(/\d{4}/); + expect(sample.gallery_link).toMatch(/^https?:\/\//); + }); + + it('falls back to a readable stand-in for variables it does not know', () => { + expect(buildPreviewSampleData(['storno_number'])).toEqual({ storno_number: '[storno_number]' }); + }); + + it('handles a template with no declared variables', () => { + expect(buildPreviewSampleData()).toEqual({}); + expect(buildPreviewSampleData([])).toEqual({}); + }); +});