Merge pull request 'fix(e2e): match an email template tab on its whole name, not a prefix' (#353) from fix/email-template-tab-name-collision into main
Linting / lint (push) Successful in 3m2s
SonarQube Analysis / sonarqube (push) Failing after 31m8s

Reviewed-on: #353
This commit was merged in pull request #353.
This commit is contained in:
2026-09-11 10:04:42 -05:00
2 changed files with 53 additions and 8 deletions
+7 -3
View File
@@ -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);
});
+46 -5
View File
@@ -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<void> {
await this.railTab(new RegExp(label)).click();
await this.railTab(label).click();
await expect(this.subject(label)).toBeVisible();
}
}