Merge branch 'main' into fix/google-button-missing-on-signup
Linting / lint (pull_request) Canceled after 0s
SonarQube Analysis / sonarqube (pull_request) Canceled after 0s

This commit is contained in:
2026-09-11 10:05:58 -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 sold',
'Favorited item withdrawn', 'Favorited item withdrawn',
'Cart reminder', '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 // Only a customised template is marked, so which ones have been changed is
// visible without opening each one. An untouched template carries nothing. // 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); await expect(adminEmails.customisedTab('Password reset')).toHaveCount(0);
}); });
+46 -5
View File
@@ -1,5 +1,29 @@
import { FrameLocator, Locator, Page, expect } from '@playwright/test'; 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. * 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}}}` }); return this.page.getByRole('button', { name: `Insert {{${name}}}` });
} }
/** One template's entry in the rail. */ /**
railTab(label: string | RegExp): Locator { * One template's entry in the rail, matched on its whole accessible name.
return this.page.getByRole('tab', { name: label }); *
* 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 { 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 { subject(label: string): Locator {
@@ -65,7 +106,7 @@ export class AdminEmails {
* resolved mid-swap finds the outgoing one. * resolved mid-swap finds the outgoing one.
*/ */
async openTemplate(label: string): Promise<void> { async openTemplate(label: string): Promise<void> {
await this.railTab(new RegExp(label)).click(); await this.railTab(label).click();
await expect(this.subject(label)).toBeVisible(); await expect(this.subject(label)).toBeVisible();
} }
} }