Fixes the only genuine failure in the first CI run after the runner was unblocked. Everything else failing in run 985 is #154.
strict mode violation: getByRole('tab', { name: /Email address changed/ })
resolved to 2 elements
The cause
AdminEmails built an unanchored regex from each label, so a template whose name merely began with another's matched both. #337 added "Email address changed by the shop" alongside "Email address changed" and broke the assertion above it.
The failure named the assertion rather than the new template, which is what makes this worth more than a rename. Same shape as the switch locator that silently retargeted in #317: a loose locator keeps passing until something new is added nearby, then fails somewhere that says nothing about the cause.
The fix
The locator, not the label. One helper builds a matcher anchored at both ends, allowing only the optional Customised suffix a tab carries once its template has been edited, and escaping the label because these are copy and copy acquires brackets and full stops eventually.
Both railTab and customisedTab go through it, and callers pass plain strings instead of assembling regexes at each call site.
The new template is added to the list the test walks, which it should have had in #337.
Testing
Verified the matcher against every real label and every colliding pair:
Label
Against
Matches
Email address changed
Email address changed
yes
Email address changed
Email address changed by the shop
no
Email address changed
Email address changed Customised
yes
Email address changed by the shop
Email address changed
no
Password reset
Password resetting
no
A customised-only match still rejects an unedited tab. Frontend tsc and lint clean.
Fixes the only genuine failure in the first CI run after the runner was unblocked. Everything else failing in run 985 is #154.
```
strict mode violation: getByRole('tab', { name: /Email address changed/ })
resolved to 2 elements
```
## The cause
`AdminEmails` built an **unanchored** regex from each label, so a template whose name merely *began* with another's matched both. #337 added "Email address changed by the shop" alongside "Email address changed" and broke the assertion above it.
The failure named the assertion rather than the new template, which is what makes this worth more than a rename. Same shape as the switch locator that silently retargeted in #317: a loose locator keeps passing until something new is added nearby, then fails somewhere that says nothing about the cause.
## The fix
The locator, not the label. One helper builds a matcher anchored at both ends, allowing only the optional `Customised` suffix a tab carries once its template has been edited, and escaping the label because these are copy and copy acquires brackets and full stops eventually.
Both `railTab` and `customisedTab` go through it, and callers pass plain strings instead of assembling regexes at each call site.
The new template is added to the list the test walks, which it should have had in #337.
## Testing
Verified the matcher against every real label and every colliding pair:
| Label | Against | Matches |
|---|---|---|
| Email address changed | Email address changed | yes |
| Email address changed | Email address changed by the shop | **no** |
| Email address changed | Email address changed Customised | yes |
| Email address changed by the shop | Email address changed | **no** |
| Password reset | Password resetting | **no** |
A customised-only match still rejects an unedited tab. Frontend `tsc` and lint clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
CI failed on a test nobody had touched:
strict mode violation: getByRole('tab', { name: /Email address changed/ })
resolved to 2 elements
The locators in AdminEmails built an unanchored regex from the label, so a template whose name merely began with another's matched both. #337 added "Email address changed by the shop" alongside "Email address changed" and broke the assertion above it.
The failure named the assertion rather than the new template, which is what made it worth more than a rename. It is the same shape as the switch locator that silently retargeted in #317: a loose locator that keeps passing until something new is added nearby, and then fails somewhere that says nothing about the cause.
So the fix is the locator rather than the label. One helper now builds a matcher anchored at both ends, allowing only the optional "Customised" suffix a tab carries once its template has been edited, and escaping the label because these are copy and copy acquires brackets and full stops eventually. Both railTab and customisedTab go through it, and callers pass plain strings instead of assembling regexes at each call site.
The new template is added to the list the test walks, which is what it should have had in #337.
Verified the matcher against every real label plus the pairs that collide, including that a customised-only match still rejects an unedited tab. Frontend tsc and lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the only genuine failure in the first CI run after the runner was unblocked. Everything else failing in run 985 is #154.
The cause
AdminEmailsbuilt an unanchored regex from each label, so a template whose name merely began with another's matched both. #337 added "Email address changed by the shop" alongside "Email address changed" and broke the assertion above it.The failure named the assertion rather than the new template, which is what makes this worth more than a rename. Same shape as the switch locator that silently retargeted in #317: a loose locator keeps passing until something new is added nearby, then fails somewhere that says nothing about the cause.
The fix
The locator, not the label. One helper builds a matcher anchored at both ends, allowing only the optional
Customisedsuffix a tab carries once its template has been edited, and escaping the label because these are copy and copy acquires brackets and full stops eventually.Both
railTabandcustomisedTabgo through it, and callers pass plain strings instead of assembling regexes at each call site.The new template is added to the list the test walks, which it should have had in #337.
Testing
Verified the matcher against every real label and every colliding pair:
A customised-only match still rejects an unedited tab. Frontend
tscand lint clean.🤖 Generated with Claude Code
CI failed on a test nobody had touched: strict mode violation: getByRole('tab', { name: /Email address changed/ }) resolved to 2 elements The locators in AdminEmails built an unanchored regex from the label, so a template whose name merely began with another's matched both. #337 added "Email address changed by the shop" alongside "Email address changed" and broke the assertion above it. The failure named the assertion rather than the new template, which is what made it worth more than a rename. It is the same shape as the switch locator that silently retargeted in #317: a loose locator that keeps passing until something new is added nearby, and then fails somewhere that says nothing about the cause. So the fix is the locator rather than the label. One helper now builds a matcher anchored at both ends, allowing only the optional "Customised" suffix a tab carries once its template has been edited, and escaping the label because these are copy and copy acquires brackets and full stops eventually. Both railTab and customisedTab go through it, and callers pass plain strings instead of assembling regexes at each call site. The new template is added to the list the test walks, which is what it should have had in #337. Verified the matcher against every real label plus the pairs that collide, including that a customised-only match still rejects an unedited tab. Frontend tsc and lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>