import { test, expect } from './fixtures'; const suffix = () => `t${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`; // Each test leaves the templates as it found them, because they are stored in // admin_settings and would otherwise change the copy a later test reads. async function restore(page: import('@playwright/test').Page, key: string) { await page.request.delete(`/api/admin/email-templates/${key}`); } async function openSettings(page: import('@playwright/test').Page) { await page.goto('/admin'); await page.getByRole('tab', { name: 'Settings' }).click(); await expect(page.getByRole('heading', { name: 'Customer emails' })).toBeVisible(); } // Opens one template's tab. Only the active tab's editor is mounted, which is // what makes the labels below unambiguous — the previous stacked layout had // every editor on screen at once and a locator for "Save" matched all six. async function openTemplate(page: import('@playwright/test').Page, label: string) { await page.getByRole('tab', { name: new RegExp(label) }).click(); await expect(page.getByLabel(`${label} subject`)).toBeVisible(); } const previewFrame = (page: import('@playwright/test').Page, label: string) => page.frameLocator(`iframe[title="${label} preview"]`); // Serial: these edit one shared stored template, and the suite runs fully // parallel by default — so run concurrently they would race, one asserting a // template is unset while another has just saved it. test.describe.configure({ mode: 'serial' }); test.describe('Editing the customer emails', () => { test.afterEach(async ({ page }) => { await restore(page, 'passwordReset'); }); test('offers every template as a tab, marked default until it is edited', async ({ page }) => { await openSettings(page); for (const label of [ 'Email verification', 'Password reset', 'Favorited item sold', 'Favorited item withdrawn', 'Cart reminder', 'Email address changed' ]) { await expect(page.getByRole('tab', { name: new RegExp(label) })).toBeVisible(); } // The badge lives on the tab now, so which templates have been changed is // visible without opening each one. await expect(page.getByRole('tab', { name: /Password reset.*Default/ })).toBeVisible(); }); test('saves a replacement subject and body', async ({ page }) => { const subject = `Reset ${suffix()}`; await openSettings(page); await openTemplate(page, 'Password reset'); await page.getByLabel('Password reset subject').fill(subject); await page .getByLabel('Password reset body') .fill('Fresh wording. [Choose a new password]({{resetUrl}}).'); await page.getByRole('button', { name: 'Save', exact: true }).click(); await expect(page.getByText('Password reset saved')).toBeVisible(); // Persisted, not merely accepted by the form. const stored = await (await page.request.get('/api/admin/email-templates')).json(); const reset = stored.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.subject).toBe(subject); }); // The assertion that matters. A body without its link still sends and still // looks fine in the log, so the save has to be refused rather than warned // about — and the admin has to be told which placeholder is missing. test('refuses a body that drops the required placeholder, and says which', async ({ page }) => { await openSettings(page); await openTemplate(page, 'Password reset'); await page.getByLabel('Password reset body').fill('Just click the thing in your email.'); await page.getByRole('button', { name: 'Save', exact: true }).click(); await expect(page.getByText('the body must keep {{resetUrl}}')).toBeVisible(); // And nothing was stored. const stored = await (await page.request.get('/api/admin/email-templates')).json(); const reset = stored.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.body).toBeNull(); }); test('restores the built-in copy', async ({ page }) => { await page.request.put('/api/admin/email-templates/passwordReset', { data: { subject: 'Temporary', body: 'Temporary [link]({{resetUrl}}).' } }); await openSettings(page); await openTemplate(page, 'Password reset'); await page.getByRole('button', { name: 'Restore default' }).click(); await expect(page.getByText('Password reset restored to the default')).toBeVisible(); const stored = await (await page.request.get('/api/admin/email-templates')).json(); const reset = stored.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.subject).toBeNull(); expect(reset.body).toBeNull(); }); }); test.describe('Previewing the customer emails', () => { test.afterEach(async ({ page }) => { await restore(page, 'passwordReset'); }); test('shows the draft being edited, not the stored copy', async ({ page }) => { const wording = `Wording ${suffix()}`; await openSettings(page); await openTemplate(page, 'Password reset'); await page .getByLabel('Password reset body') .fill(`${wording}. [Choose a new password]({{resetUrl}}).`); // Nothing has been saved. The preview still reflects it, which is the whole // point: an admin sees the effect before committing to it. await expect(previewFrame(page, 'Password reset').getByText(wording)).toBeVisible(); const stored = await (await page.request.get('/api/admin/email-templates')).json(); expect(stored.find((t: { key: string }) => t.key === 'passwordReset').body).toBeNull(); }); test('substitutes sample values rather than showing raw placeholders', async ({ page }) => { await openSettings(page); await openTemplate(page, 'Password reset'); const frame = previewFrame(page, 'Password reset'); await expect(frame.getByRole('link')).toHaveAttribute('href', /reset-password\?token=/); await expect(frame.locator('body')).not.toContainText('{{resetUrl}}'); }); // The control that stops an admin putting script into a customer's inbox is // markdown-it's html: false on the server. The preview has to show the same // thing the mailer emits, or it would be reassuring about the wrong output. test('escapes raw HTML exactly as the mailer does', async ({ page }) => { await openSettings(page); await openTemplate(page, 'Password reset'); await page .getByLabel('Password reset body') .fill(' [link]({{resetUrl}})'); await expect(previewFrame(page, 'Password reset').getByText('')).toBeVisible(); }); // Appended by the server and not editable, so it has to appear in the preview // of the two templates it belongs to and nowhere else. test('includes the consent footer on a favorite template, and not on others', async ({ page }) => { await openSettings(page); await openTemplate(page, 'Favorited item sold'); await expect(previewFrame(page, 'Favorited item sold').getByText(/account page/)).toBeVisible(); await openTemplate(page, 'Password reset'); await expect(previewFrame(page, 'Password reset').locator('body')).not.toContainText('account page'); }); });