import { FrameLocator, Locator, Page, expect } from '@playwright/test'; /** * The Emails tab: a vertical rail of template types and one editor at a time. * * Only the active template's editor is mounted, which is what keeps the labels * below unambiguous — the previous stacked layout had every editor on screen at * once and a locator for "Save" matched all six (#135). * * A customised template is marked in the rail with a dot carrying an * aria-label, so the state is in the tab's accessible name rather than colour * alone — which is what lets a test assert it without opening the editor. */ export class AdminEmails { readonly saveButton: Locator; readonly restoreDefaultButton: Locator; constructor(private readonly page: Page) { this.saveButton = page.getByRole('button', { name: 'Save', exact: true }); this.restoreDefaultButton = page.getByRole('button', { name: 'Restore default' }); } /** One template's entry in the rail. */ railTab(label: string | RegExp): Locator { return this.page.getByRole('tab', { name: label }); } customisedTab(label: string): Locator { return this.page.getByRole('tab', { name: new RegExp(`${label}.*Customised`) }); } subject(label: string): Locator { return this.page.getByLabel(`${label} subject`); } body(label: string): Locator { return this.page.getByLabel(`${label} body`); } /** * The rendered preview, which is an iframe. * * It is the server's rendering of the actual email rather than the markdown * editor's own, so it is the only thing that shows the consent footer and the * substituted placeholders. */ preview(label: string): FrameLocator { return this.page.frameLocator(`iframe[title="${label} preview"]`); } /** * Opens one template and waits for its editor. * * The wait is the action's contract: the rail swaps the editor, and a locator * resolved mid-swap finds the outgoing one. */ async openTemplate(label: string): Promise { await this.railTab(new RegExp(label)).click(); await expect(this.subject(label)).toBeVisible(); } }