diff --git a/frontend/tests/e2e/email-templates.spec.ts b/frontend/tests/e2e/email-templates.spec.ts index 48fac51..c62b10f 100644 --- a/frontend/tests/e2e/email-templates.spec.ts +++ b/frontend/tests/e2e/email-templates.spec.ts @@ -25,14 +25,18 @@ test.describe('Editing the customer emails', () => { 'Favorited item sold', 'Favorited item withdrawn', 'Cart reminder', - 'Email address changed' + '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(new RegExp(label))).toBeVisible(); + 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.railTab('Password reset')).toBeVisible(); await expect(adminEmails.customisedTab('Password reset')).toHaveCount(0); }); diff --git a/frontend/tests/e2e/pages/AdminEmails.ts b/frontend/tests/e2e/pages/AdminEmails.ts index 6bb62c7..c4044a6 100644 --- a/frontend/tests/e2e/pages/AdminEmails.ts +++ b/frontend/tests/e2e/pages/AdminEmails.ts @@ -1,5 +1,29 @@ import { FrameLocator, Locator, Page, expect } from '@playwright/test'; +/** + * A matcher for one template label, against a tab's *whole* accessible name. + * + * A tab is named for its template, plus the word "Customised" once it has been + * edited — the dot beside it carries that as an aria-label, so the state is not + * colour-only. + * + * Anchored at both ends, which is the entire point of this function. The + * locators here used to build an unanchored regex from the label, so a template + * whose name merely *began* with another's matched both. Adding "Email address + * changed by the shop" alongside "Email address changed" broke a passing test + * with a strict-mode violation naming the assertion rather than the new + * template — the same shape as the switch locator that silently retargeted in + * #317, and the same cost to diagnose. + * + * The escape matters for the same reason: these labels are copy, and copy + * acquires brackets and full stops eventually. + */ +function nameMatching(label: string, options: { customised?: boolean } = {}): RegExp { + const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const suffix = options.customised ? '\\s+Customised' : '(?:\\s+Customised)?'; + return new RegExp(`^${escaped}${suffix}$`); +} + /** * The Emails tab: a vertical rail of template types and one editor at a time. * @@ -30,13 +54,30 @@ export class AdminEmails { return this.page.getByRole('button', { name: `Insert {{${name}}}` }); } - /** One template's entry in the rail. */ - railTab(label: string | RegExp): Locator { - return this.page.getByRole('tab', { name: label }); + /** + * One template's entry in the rail, matched on its whole accessible name. + * + * A tab's accessible name is the template's label, plus the word "Customised" + * when it has been edited — the dot beside it carries that as an aria-label so + * the state is not colour-only. + * + * Anchored at both ends, which is the point of this helper rather than a bare + * substring match. These locators used to build an unanchored regex from the + * label, so a template whose name merely *began* with another's matched both. + * Adding "Email address changed by the shop" beside "Email address changed" + * broke a passing test with a strict-mode violation, and the failure named the + * assertion rather than the new template — the same shape as the switch + * locator that silently retargeted in #317. + * + * The escape matters for the same reason: a label is copy, and copy acquires + * brackets and full stops eventually. + */ + railTab(label: string): Locator { + return this.page.getByRole('tab', { name: nameMatching(label) }); } customisedTab(label: string): Locator { - return this.page.getByRole('tab', { name: new RegExp(`${label}.*Customised`) }); + return this.page.getByRole('tab', { name: nameMatching(label, { customised: true }) }); } subject(label: string): Locator { @@ -65,7 +106,7 @@ export class AdminEmails { * resolved mid-swap finds the outgoing one. */ async openTemplate(label: string): Promise { - await this.railTab(new RegExp(label)).click(); + await this.railTab(label).click(); await expect(this.subject(label)).toBeVisible(); } }