import { test, expect, uniqueSuffix, findOrFail } from './fixtures'; // 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(request: import('@playwright/test').APIRequestContext, key: string) { await request.delete(`/api/admin/email-templates/${key}`); } // 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, admin, adminEmails }) => { await restore(page.request, 'passwordReset'); }); test('offers every template as a tab, marked default until it is edited', async ({ page, admin, adminEmails }) => { await admin.open('Emails'); for (const label of [ 'Email verification', 'Password reset', 'Favorited item sold', 'Favorited item withdrawn', 'Cart reminder', 'Email address changed', // Added in #337, and the reason these are matched on the whole // accessible name rather than as substrings: it extends the label above // it, so an unanchored match resolved to both tabs. 'Email address changed by the shop' ]) { await expect(adminEmails.railTab(label)).toBeVisible(); } // Only a customised template is marked, so which ones have been changed is // visible without opening each one. An untouched template carries nothing. await expect(adminEmails.railTab('Password reset')).toBeVisible(); await expect(adminEmails.customisedTab('Password reset')).toHaveCount(0); }); test('saves a replacement subject and body', async ({ page, admin, adminEmails }) => { const subject = `Reset ${uniqueSuffix()}`; await admin.open('Emails'); await adminEmails.openTemplate('Password reset'); await adminEmails.subject('Password reset').fill(subject); await page .getByLabel('Password reset body') .fill('Fresh wording. [Choose a new password]({{resetUrl}}).'); await adminEmails.saveButton.click(); await expect(page.getByText('Password reset saved')).toBeVisible(); // Persisted, not merely accepted by the form. const stored: { key: string; subject: string | null; body: string | null }[] = await ( await page.request.get('/api/admin/email-templates') ).json(); const reset = findOrFail( stored, (t) => t.key === 'passwordReset', 'the passwordReset template' ); 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, admin, adminEmails }) => { await admin.open('Emails'); await adminEmails.openTemplate('Password reset'); await adminEmails.body('Password reset').fill('Just click the thing in your email.'); await adminEmails.saveButton.click(); await expect(page.getByText('the body must keep {{resetUrl}}')).toBeVisible(); // And nothing was stored. const stored: { key: string; subject: string | null; body: string | null }[] = await ( await page.request.get('/api/admin/email-templates') ).json(); const reset = findOrFail( stored, (t) => t.key === 'passwordReset', 'the passwordReset template' ); expect(reset.body).toBeNull(); }); test('restores the built-in copy', async ({ page, admin, adminEmails }) => { await page.request.put('/api/admin/email-templates/passwordReset', { data: { subject: 'Temporary', body: 'Temporary [link]({{resetUrl}}).' } }); await admin.open('Emails'); // Marked in the rail before it is opened, which is the whole point of the // dot — the stored template above was never touched through the UI. await expect(adminEmails.customisedTab('Password reset')).toBeVisible(); await adminEmails.openTemplate('Password reset'); await adminEmails.restoreDefaultButton.click(); await expect(page.getByText('Password reset restored to the default')).toBeVisible(); const stored: { key: string; subject: string | null; body: string | null }[] = await ( await page.request.get('/api/admin/email-templates') ).json(); const reset = findOrFail( stored, (t) => t.key === 'passwordReset', 'the passwordReset template' ); expect(reset.subject).toBeNull(); expect(reset.body).toBeNull(); }); }); test.describe('Previewing the customer emails', () => { test.afterEach(async ({ page, admin, adminEmails }) => { await restore(page.request, 'passwordReset'); }); test('shows the draft being edited, not the stored copy', async ({ page, admin, adminEmails }) => { const wording = `Wording ${uniqueSuffix()}`; await admin.open('Emails'); await adminEmails.openTemplate('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(adminEmails.preview('Password reset').getByText(wording)).toBeVisible(); const stored: { key: string; subject: string | null; body: string | null }[] = await ( await page.request.get('/api/admin/email-templates') ).json(); expect(findOrFail( stored, (t) => t.key === 'passwordReset', 'the passwordReset template' ).body).toBeNull(); }); test('substitutes sample values rather than showing raw placeholders', async ({ page, admin, adminEmails }) => { await admin.open('Emails'); await adminEmails.openTemplate('Password reset'); const frame = adminEmails.preview('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, admin, adminEmails }) => { await admin.open('Emails'); await adminEmails.openTemplate('Password reset'); await page .getByLabel('Password reset body') .fill(' [link]({{resetUrl}})'); await expect(adminEmails.preview('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, admin, adminEmails }) => { await admin.open('Emails'); await adminEmails.openTemplate('Favorited item sold'); await expect(adminEmails.preview('Favorited item sold').getByText(/account page/)).toBeVisible(); await adminEmails.openTemplate('Password reset'); await expect(adminEmails.preview('Password reset').locator('body')).not.toContainText('account page'); }); });