Playwright matches an accessible name case-insensitively and as a substring unless exact is passed, so `getByRole('button', { name: 'OK' })` matched any button whose name merely contained "ok".
Caught on a full serial run of main:
strict mode violation: getByRole('button', { name: 'OK' }) resolved to 2 elements:
1) aka getByRole('button', { name: 'Freed rmtiq9okg22k9e' })
2) aka getByRole('button', { name: 'OK', exact: true })
A leftover "Freed …" toast was still on screen and the random base36 suffix happened to contain "ok". Roughly one suffix in a few hundred does, which is the profile of a test that fails occasionally and reproduces for nobody.
This is not the contention #241 addressed. It reproduced with workers: 1, serially, on a fresh database — it needs only a stale toast and an unlucky suffix. It is at least part of what #245 skipped a real test to work around.
Also makes the taxonomy modal's OK and the sold-filter 'All' radio exact, the only other targets short enough to appear inside a random suffix. The dozen or so remaining short names are left alone deliberately: they would need a collision in deliberately-chosen test data rather than a random one, and substring matching is load-bearing in some of them.
Verified with two consecutive full e2e passes, 155 of 155 both times.
Closes #253
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
import { Locator, Page } from '@playwright/test';
|
|
|
|
/**
|
|
* The Categories and Tags tabs, which share a shape: a table or tree, an
|
|
* "Add X" button, and an antd Modal with a single Name field behind it.
|
|
*
|
|
* Categories are a tree because they nest arbitrarily deep; tags are flat. The
|
|
* tree is what makes "Add child" meaningful, and it is mounted before a new
|
|
* branch exists — so a child is only visible if expansion follows newly created
|
|
* nodes rather than the state captured at first render, which is the thing the
|
|
* spec here pins down.
|
|
*/
|
|
export class AdminTaxonomy {
|
|
readonly addCategoryButton: Locator;
|
|
readonly addTagButton: Locator;
|
|
readonly name: Locator;
|
|
readonly confirmButton: Locator;
|
|
readonly tagAddedNotice: Locator;
|
|
|
|
constructor(private readonly page: Page) {
|
|
this.addCategoryButton = page.getByRole('button', { name: 'Add Category' });
|
|
this.addTagButton = page.getByRole('button', { name: 'Add Tag' });
|
|
this.name = page.getByLabel('Name');
|
|
// Substring matching would collide with random suffixes. See #253.
|
|
this.confirmButton = page.getByRole('button', { name: 'OK', exact: true });
|
|
this.tagAddedNotice = page.getByText('Tag added');
|
|
}
|
|
|
|
categoryNode(name: string): Locator {
|
|
return this.page.getByRole('treeitem').filter({ hasText: name });
|
|
}
|
|
|
|
async addCategory(name: string): Promise<void> {
|
|
await this.addCategoryButton.click();
|
|
await this.name.fill(name);
|
|
await this.confirmButton.click();
|
|
}
|
|
|
|
async addChildCategory(parentName: string, childName: string): Promise<void> {
|
|
await this.categoryNode(parentName).getByRole('button', { name: 'Add child' }).click();
|
|
await this.name.fill(childName);
|
|
await this.confirmButton.click();
|
|
}
|
|
|
|
async addTag(name: string): Promise<void> {
|
|
await this.addTagButton.click();
|
|
await this.name.fill(name);
|
|
await this.confirmButton.click();
|
|
}
|
|
}
|