Email template helpers: keeping repeated interactions out of tests#
Several TOL applications handled email templates in a similar way. I created reusable functions for those interactions so a test could describe the template it was checking without repeating every control lookup. When an application introduced a variation, I could update the shared function or supply an option and keep the test readable.
This page follows the complete implementations of verifyTemplateFields, setReminderHours, and interactWithIframeCell, then shows calls with inline values and a JSON configuration. The tests and JSON below are illustrative examples using the existing APIs; their template labels and screen URL must match the application being tested.
Open a template and handle its optional sections#
The complete verifyTemplateFields function shows where the repeated browser actions live:
export async function verifyTemplateFields(
page: Page,
templateName: string,
options: { hasHelperTags?: boolean; hasNotice?: boolean } = {}
) {
const { hasHelperTags = true, hasNotice = true } = options;
console.log(`Verifying template: ${templateName}`);
await page.getByRole('button', { name: templateName }).click();
await page.waitForTimeout(500);
await page.getByText('Type', { exact: true }).click();
await page.waitForTimeout(300);
if (hasHelperTags) {
try {
await page.getByText('Helper Tags').click();
await page.waitForTimeout(300);
} catch {
console.log(`Helper Tags not found for ${templateName}`);
}
}
if (hasNotice) {
try {
await page.getByText('Notice').click();
await page.waitForTimeout(300);
} catch {
console.log(`Notice not found for ${templateName}`);
}
}
}
The first argument is the active browser page. templateName supplies the button label used to open a template. The options object controls the two optional sections; both options default to true, so a caller only needs to specify a difference.
For example, hasNotice: false skips the Notice interaction for a template that does not have that section. A change to the shared Type control can be repaired inside this function. A variation that changes the workflow substantially may need a separate helper; adding another option only helps when the operation still has the same purpose.
The function’s current contract is narrower than its name might suggest. It opens controls, and it logs failures to find Helper Tags or Notice. It does not assert the template’s subject or body, save changes, send an email, or check delivery. Those would require their own actions and assertions.
Fill reminders within the correct region#
Reminder labels can recur on the same screen. setReminderHours first identifies an accessible region, then passes that region into safeFill2 so each field lookup is scoped to that part of the page.
export async function setReminderHours(
page: Page,
region: string,
reminderFields: { first?: string; second?: string; afterSecond?: string }
) {
const regionLocator = page.getByRole('region', { name: region });
if (reminderFields.first) {
await safeFill2(page, 'Number of Hours for First Reminder', reminderFields.first, regionLocator);
}
if (reminderFields.second) {
await safeFill2(page, 'Number of Hours for Second Reminder', reminderFields.second, regionLocator);
}
if (reminderFields.afterSecond) {
await safeFill2(page, 'Number of Hours After Second Reminder', reminderFields.afterSecond, regionLocator);
}
}
first, second, and afterSecond are strings because form-filling operations receive text. Omitted values are skipped. The truthiness checks also skip an empty string, so this API cannot express “clear this field” by passing ''. A value such as '0' is still a nonempty string and reaches the filling helper.
Reach a cell inside the email iframe#
Some template interactions occur in embedded content. This helper keeps the frame lookup out of the scenario:
export async function interactWithIframeCell(page: Page, cellText: string) {
const frame = await page.locator('iframe').first().contentFrame();
if (frame) {
await frame.getByRole('cell', { name: cellText }).click();
} else {
throw new Error('Iframe content not accessible');
}
}
The helper targets the first iframe and clicks a cell matching cellText. It does not edit an arbitrary rich-text body. If another application introduces more than one relevant iframe, the frame choice is an implementation detail to revisit in this helper.
A complete test with inline values#
This illustrative example uses EMAIL_TEMPLATE_TEST_URL for the authenticated template screen’s URL. The existing login helper reads the suite’s normal login configuration. Login and navigation run in beforeEach; Playwright owns and closes each test’s page fixture.
The example assumes the Welcome Email screen contains the reminder region and iframe cell shown below. Replace these sample labels with the actual labels for the template under test.
import { test } from '@playwright/test';
import { loginAsSuperAdmin } from '../../HelperFiles/cit-app-helpers';
import {
verifyTemplateFields,
setReminderHours,
interactWithIframeCell
} from '../../HelperFiles/components/Email/email-template-components';
const templateScreenUrl = process.env.EMAIL_TEMPLATE_TEST_URL;
if (!templateScreenUrl) {
throw new Error('Set EMAIL_TEMPLATE_TEST_URL to the template screen');
}
test.describe('Email template interactions', () => {
test.beforeEach(async ({ page }) => {
await loginAsSuperAdmin(page);
await page.goto(templateScreenUrl);
});
test('Inspect the welcome template and enter reminders', async ({ page }) => {
await verifyTemplateFields(page, 'Welcome Email', {
hasHelperTags: true,
hasNotice: false
});
await setReminderHours(page, 'Reminder Settings', {
first: '24',
second: '48',
afterSecond: '72'
});
await interactWithIframeCell(page, 'Email Body');
});
test('Inspect a template that includes a Notice section', async ({ page }) => {
await verifyTemplateFields(page, 'Account Notice', {
hasHelperTags: false,
hasNotice: true
});
});
});
Each test starts from the template screen, so opening one template does not determine the next test’s starting state. The test body records which template and values matter. The shared module contains the frame, region, label, and wait handling.
The same workflow with JSON configuration#
If the values already live in a configuration file, the helper API stays the same. This is an illustrative email-template.json, placed at the suite root:
{
"templateName": "Welcome Email",
"options": {
"hasHelperTags": true,
"hasNotice": false
},
"reminderRegion": "Reminder Settings",
"reminderHours": {
"first": "24",
"second": "48",
"afterSecond": "72"
},
"iframeCellText": "Email Body"
}
Here is the complete alternative spec. Run from the suite root so the JSON path resolves. Explicit file reading avoids requiring JSON-import settings in the TypeScript configuration.
import { test } from '@playwright/test';
import { readFileSync } from 'node:fs';
import { loginAsSuperAdmin } from '../../HelperFiles/cit-app-helpers';
import {
verifyTemplateFields,
setReminderHours,
interactWithIframeCell
} from '../../HelperFiles/components/Email/email-template-components';
type EmailTemplateConfig = {
templateName: string;
options: Parameters<typeof verifyTemplateFields>[2];
reminderRegion: string;
reminderHours: Parameters<typeof setReminderHours>[2];
iframeCellText: string;
};
const config: EmailTemplateConfig = JSON.parse(
readFileSync('email-template.json', 'utf8')
);
const templateScreenUrl = process.env.EMAIL_TEMPLATE_TEST_URL;
if (!templateScreenUrl) {
throw new Error('Set EMAIL_TEMPLATE_TEST_URL to the template screen');
}
test.describe('Configured email template interactions', () => {
test.beforeEach(async ({ page }) => {
await loginAsSuperAdmin(page);
await page.goto(templateScreenUrl);
});
test('Inspect the template and enter configured reminders', async ({ page }) => {
await verifyTemplateFields(page, config.templateName, config.options);
await setReminderHours(page, config.reminderRegion, config.reminderHours);
await interactWithIframeCell(page, config.iframeCellText);
});
});
Parameters keeps the illustrated TypeScript shape tied to the helper’s arguments. It does not validate parsed JSON at runtime; this example assumes the checked-in configuration has the displayed shape. JSON supplies labels, options, and values. It does not introduce a new JSON argument into any of the three functions.
Where this helper fits in the larger suite#
This is the reuse pattern developed in suite evolution: identify repeated behavior, give it a named API, and keep variations close to that implementation. The agent workflow later uses documented functions like these as building blocks for generated tests. When a selector fails, the diagnostic walkthrough shows how browser evidence can guide a change to the helper.
Continue to primary and mailing addresses, or return to Functions explained.