Skip to content

Commit c09bb88

Browse files
frano-mclaude
andauthored
test: stabilize and rewrite anvil-cmg filter e2e suite (#4800) (#4801)
* test: stabilize and rewrite anvil-cmg filter e2e suite (#4800) rewrites e2e/anvil/anvil-filters.spec.ts in the style of the anvil-catalog filter spec to fix a flaky biosamples chip-click test and reduce overall flake surface. key changes: - replaced positional/text-based locators with test-id locators (filter-popover, filter-item, filter-term, filter-count, table-first-cell, clear-all-filters, filters) - extracted shared filter helpers to e2e/features/common/filters.ts so both anvil-cmg and anvil-catalog specs use the same primitives; generic helpers to e2e/features/common/utils.ts (escapeRegExp) - option clicks via dispatchevent (matches existing sidebar-button pattern, avoids webkit click swallowing) - popover assertions re-locate by name across facets (filter list re-sorts when count changes); within a single facet popover, positional locators are kept since the list does not reorder on selection - filter tag chips matched via mui-chip-root scoped to the filters container, replacing brittle #sidebar-positioner text lookups - consolidated 10 per-entity filter-presence/open tests into 2 (files + donors representatives) covering both strict-list match and per-filter open/close cleanup: - removed testfilterpersistence, testfiltercounts, testfiltertags, testclearall, testfilterpresence and their now-orphaned helpers from e2e/testFunctions.ts - demoted getmthrowncthcolumncelllocator and getfirstrownthcolumncelllocator from export to internal const - removed unused anvil_files_selectable_columns_by_name from e2e/anvil/constants.ts Closes #4800 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: use dispatchevent for popover item clicks (#4800) per copilot review on #4801: the multi-select and count tests still used regular click() on popover items, which can be swallowed by the popover overlay on webkit (the same failure mode the selectFirstOption helper guards against). switch both to dispatchevent("click") for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Fran McDade <18710366+frano-m@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bc517c8 commit c09bb88

6 files changed

Lines changed: 520 additions & 713 deletions

File tree

Lines changed: 17 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import { expect, Locator, Page, test } from "@playwright/test";
1+
import type { Locator } from "@playwright/test";
2+
import { expect, test } from "@playwright/test";
3+
import { TEST_IDS } from "../features/common/constants";
24
import {
3-
KEYBOARD_KEYS,
4-
MUI_CLASSES,
5-
TEST_IDS,
6-
} from "../features/common/constants";
5+
closeAutocompletePopper,
6+
closeFilterPopover,
7+
expectFilterItemNotSelected,
8+
expectFilterItemSelected,
9+
fillSearchAllFilters,
10+
filterTag,
11+
getFirstOptionName,
12+
namedFilterItem,
13+
openFilterDropdown,
14+
selectFirstOption,
15+
} from "../features/common/filters";
716
import { ANVIL_CATALOG_CATEGORY_NAMES } from "./constants";
817

918
const ENTITIES = [
@@ -36,17 +45,15 @@ test.describe("AnVIL Catalog filter search", () => {
3645
// Open filter dropdown, note first option name, close
3746
await openFilterDropdown(filters, filterName);
3847
const optionName = await getFirstOptionName(page);
39-
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
40-
await expectFilterPopoverClosed(page);
48+
await closeFilterPopover(page);
4149

4250
// Search for the option and select it
4351
await fillSearchAllFilters(searchAllFilters, optionName);
4452
const filterItem = namedFilterItem(page, optionName);
4553
await expectFilterItemNotSelected(filterItem);
4654
await filterItem.click();
4755
await expectFilterItemSelected(filterItem);
48-
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
49-
await expectAutocompletePopperClosed(page);
56+
await closeAutocompletePopper(page);
5057

5158
// Verify filter tag appeared
5259
await expect(filterTag(filters, optionName)).toBeVisible();
@@ -69,8 +76,7 @@ test.describe("AnVIL Catalog filter search", () => {
6976
await expectFilterItemSelected(filterItem);
7077
await filterItem.click();
7178
await expectFilterItemNotSelected(filterItem);
72-
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
73-
await expectAutocompletePopperClosed(page);
79+
await closeAutocompletePopper(page);
7480

7581
// Verify filter tag disappeared
7682
await expect(filterTag(filters, optionName)).not.toBeVisible();
@@ -79,205 +85,3 @@ test.describe("AnVIL Catalog filter search", () => {
7985
});
8086
}
8187
});
82-
83-
/* ——————————————————————————— helpers ——————————————————————————— */
84-
85-
/**
86-
* Escapes regex special characters in a string.
87-
* @param s - The string to escape.
88-
* @returns A string with all RegExp special characters escaped.
89-
*/
90-
function escapeRegExp(s: string): string {
91-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92-
}
93-
94-
/**
95-
* Waits for the autocomplete popper to be fully unmounted from the DOM.
96-
* @param page - Page.
97-
*/
98-
async function expectAutocompletePopperClosed(page: Page): Promise<void> {
99-
await expect(page.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)).toHaveCount(0);
100-
}
101-
102-
/**
103-
* Waits for the autocomplete popper to be visible.
104-
* @param page - Page.
105-
*/
106-
async function expectAutocompletePopperOpen(page: Page): Promise<void> {
107-
await expect(page.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)).toBeVisible();
108-
}
109-
110-
/**
111-
* Asserts that a filter item is not selected.
112-
* @param filterItem - A filter-item locator.
113-
*/
114-
async function expectFilterItemNotSelected(filterItem: Locator): Promise<void> {
115-
await expect(filterItem).not.toHaveClass(/Mui-selected/);
116-
}
117-
118-
/**
119-
* Asserts that a filter item is selected.
120-
* @param filterItem - A filter-item locator.
121-
*/
122-
async function expectFilterItemSelected(filterItem: Locator): Promise<void> {
123-
await expect(filterItem).toHaveClass(/Mui-selected/);
124-
}
125-
126-
/**
127-
* Waits for all filter popovers to be fully unmounted from the DOM.
128-
* @param page - Page.
129-
*/
130-
async function expectFilterPopoverClosed(page: Page): Promise<void> {
131-
await expect(filterPopover(page)).toHaveCount(0);
132-
}
133-
134-
/**
135-
* Waits for the filter popover to be visible.
136-
* @param page - Page.
137-
*/
138-
async function expectFilterPopoverOpen(page: Page): Promise<void> {
139-
await expect(filterPopover(page)).toBeVisible();
140-
}
141-
142-
/**
143-
* Extracts the display name from a filter item element.
144-
* @param filterItem - A locator for the filter-item element.
145-
* @returns The display name of the filter option.
146-
*/
147-
async function extractOptionName(filterItem: Locator): Promise<string> {
148-
return (
149-
await filterItem.getByTestId(TEST_IDS.FILTER_TERM).innerText()
150-
).trim();
151-
}
152-
153-
/**
154-
* Fills the "Search all filters" input and waits for the results to appear.
155-
* @param searchAllFilters - The search-all-filters container locator.
156-
* @param text - The text to type into the search input.
157-
*/
158-
async function fillSearchAllFilters(
159-
searchAllFilters: Locator,
160-
text: string
161-
): Promise<void> {
162-
await expectAutocompletePopperClosed(searchAllFilters.page());
163-
const input = searchAllFilters.getByRole("combobox");
164-
await input.fill(text);
165-
await expectAutocompletePopperOpen(searchAllFilters.page());
166-
}
167-
168-
/**
169-
* Returns a locator for the filter popover.
170-
* @param page - Page.
171-
* @returns A locator for the filter popover.
172-
*/
173-
function filterPopover(page: Page): Locator {
174-
return page.getByTestId(TEST_IDS.FILTER_POPOVER);
175-
}
176-
177-
/**
178-
* Returns a regex matching a sidebar filter button, e.g. "Consent Code (5)".
179-
* @param filterName - The name of the filter.
180-
* @returns A RegExp matching the sidebar button text.
181-
*/
182-
function filterRegex(filterName: string): RegExp {
183-
return new RegExp(escapeRegExp(filterName) + "\\s+\\(\\d+\\)\\s*");
184-
}
185-
186-
/**
187-
* Returns a locator for a filter tag (MuiChip) within the filters container.
188-
* @param filters - The filters container locator.
189-
* @param name - The filter option name to match.
190-
* @returns A locator for the filter tag chip.
191-
*/
192-
function filterTag(filters: Locator, name: string): Locator {
193-
return filters.locator(MUI_CLASSES.CHIP, { hasText: name });
194-
}
195-
196-
/**
197-
* Returns a locator for the first filter item in the open popover.
198-
* @param page - Page.
199-
* @returns A locator for the first filter item.
200-
*/
201-
function firstFilterItem(page: Page): Locator {
202-
return filterPopover(page).getByTestId(TEST_IDS.FILTER_ITEM).first();
203-
}
204-
205-
/**
206-
* Returns the name of the first filter item in the open popover.
207-
* @param page - Page.
208-
* @returns The display name of the first option.
209-
*/
210-
async function getFirstOptionName(page: Page): Promise<string> {
211-
return extractOptionName(firstFilterItem(page));
212-
}
213-
214-
/**
215-
* Returns a locator for a named filter item in the autocomplete popper.
216-
* @param page - Page.
217-
* @param optionName - The display name of the filter option.
218-
* @returns A locator for the matching filter item.
219-
*/
220-
function namedFilterItem(page: Page, optionName: string): Locator {
221-
return page
222-
.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)
223-
.getByTestId(TEST_IDS.FILTER_ITEM)
224-
.filter({ hasText: RegExp(`^${escapeRegExp(optionName)}\\s*\\d+\\s*`) })
225-
.first();
226-
}
227-
228-
/**
229-
* Opens a filter dropdown by clicking its sidebar button.
230-
* Uses dispatchEvent because the filter menu sometimes intercepts regular clicks.
231-
* @param filters - The filters container locator.
232-
* @param filterName - The name of the sidebar filter to open.
233-
*/
234-
async function openFilterDropdown(
235-
filters: Locator,
236-
filterName: string
237-
): Promise<void> {
238-
await expectFilterPopoverClosed(filters.page());
239-
const button = filters.getByText(filterRegex(filterName));
240-
await expect(button).toBeVisible();
241-
await button.dispatchEvent("click");
242-
await expectFilterPopoverOpen(filters.page());
243-
}
244-
245-
/**
246-
* Returns a locator for a named filter item in the open filter popover.
247-
* @param page - Page.
248-
* @param optionName - The display name of the filter option.
249-
* @returns A locator for the matching filter item.
250-
*/
251-
function namedPopoverFilterItem(page: Page, optionName: string): Locator {
252-
return filterPopover(page)
253-
.getByTestId(TEST_IDS.FILTER_ITEM)
254-
.filter({ hasText: new RegExp(`^${escapeRegExp(optionName)}\\s*\\d+\\s*`) })
255-
.first();
256-
}
257-
258-
/**
259-
* Opens a sidebar filter dropdown, selects its first option, and returns the
260-
* option name. Waits for the item to be selected before returning.
261-
* @param filters - The filters container locator.
262-
* @param page - Page (needed for popover content).
263-
* @param filterName - The name of the sidebar filter to open.
264-
* @returns The display name of the selected option.
265-
*/
266-
async function selectFirstOption(
267-
filters: Locator,
268-
page: Page,
269-
filterName: string
270-
): Promise<string> {
271-
await openFilterDropdown(filters, filterName);
272-
const option = firstFilterItem(page);
273-
const name = await extractOptionName(option);
274-
await expectFilterItemNotSelected(option);
275-
await option.click();
276-
// Re-locate by name rather than position since the list may re-sort after
277-
// selection, causing the positional `.first()` locator to resolve to a
278-
// different (non-selected) element.
279-
await expectFilterItemSelected(namedPopoverFilterItem(page, name));
280-
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
281-
await expectFilterPopoverClosed(page);
282-
return name;
283-
}

0 commit comments

Comments
 (0)