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');
});
});