Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 17 additions & 213 deletions e2e/anvil-catalog/anvilcatalog-filters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { expect, Locator, Page, test } from "@playwright/test";
import type { Locator } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { TEST_IDS } from "../features/common/constants";
import {
KEYBOARD_KEYS,
MUI_CLASSES,
TEST_IDS,
} from "../features/common/constants";
closeAutocompletePopper,
closeFilterPopover,
expectFilterItemNotSelected,
expectFilterItemSelected,
fillSearchAllFilters,
filterTag,
getFirstOptionName,
namedFilterItem,
openFilterDropdown,
selectFirstOption,
} from "../features/common/filters";
import { ANVIL_CATALOG_CATEGORY_NAMES } from "./constants";

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

// Search for the option and select it
await fillSearchAllFilters(searchAllFilters, optionName);
const filterItem = namedFilterItem(page, optionName);
await expectFilterItemNotSelected(filterItem);
await filterItem.click();
await expectFilterItemSelected(filterItem);
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
await expectAutocompletePopperClosed(page);
await closeAutocompletePopper(page);

// Verify filter tag appeared
await expect(filterTag(filters, optionName)).toBeVisible();
Expand All @@ -69,8 +76,7 @@ test.describe("AnVIL Catalog filter search", () => {
await expectFilterItemSelected(filterItem);
await filterItem.click();
await expectFilterItemNotSelected(filterItem);
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
await expectAutocompletePopperClosed(page);
await closeAutocompletePopper(page);

// Verify filter tag disappeared
await expect(filterTag(filters, optionName)).not.toBeVisible();
Expand All @@ -79,205 +85,3 @@ test.describe("AnVIL Catalog filter search", () => {
});
}
});

/* ——————————————————————————— helpers ——————————————————————————— */

/**
* Escapes regex special characters in a string.
* @param s - The string to escape.
* @returns A string with all RegExp special characters escaped.
*/
function escapeRegExp(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
* Waits for the autocomplete popper to be fully unmounted from the DOM.
* @param page - Page.
*/
async function expectAutocompletePopperClosed(page: Page): Promise<void> {
await expect(page.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)).toHaveCount(0);
}

/**
* Waits for the autocomplete popper to be visible.
* @param page - Page.
*/
async function expectAutocompletePopperOpen(page: Page): Promise<void> {
await expect(page.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)).toBeVisible();
}

/**
* Asserts that a filter item is not selected.
* @param filterItem - A filter-item locator.
*/
async function expectFilterItemNotSelected(filterItem: Locator): Promise<void> {
await expect(filterItem).not.toHaveClass(/Mui-selected/);
}

/**
* Asserts that a filter item is selected.
* @param filterItem - A filter-item locator.
*/
async function expectFilterItemSelected(filterItem: Locator): Promise<void> {
await expect(filterItem).toHaveClass(/Mui-selected/);
}

/**
* Waits for all filter popovers to be fully unmounted from the DOM.
* @param page - Page.
*/
async function expectFilterPopoverClosed(page: Page): Promise<void> {
await expect(filterPopover(page)).toHaveCount(0);
}

/**
* Waits for the filter popover to be visible.
* @param page - Page.
*/
async function expectFilterPopoverOpen(page: Page): Promise<void> {
await expect(filterPopover(page)).toBeVisible();
}

/**
* Extracts the display name from a filter item element.
* @param filterItem - A locator for the filter-item element.
* @returns The display name of the filter option.
*/
async function extractOptionName(filterItem: Locator): Promise<string> {
return (
await filterItem.getByTestId(TEST_IDS.FILTER_TERM).innerText()
).trim();
}

/**
* Fills the "Search all filters" input and waits for the results to appear.
* @param searchAllFilters - The search-all-filters container locator.
* @param text - The text to type into the search input.
*/
async function fillSearchAllFilters(
searchAllFilters: Locator,
text: string
): Promise<void> {
await expectAutocompletePopperClosed(searchAllFilters.page());
const input = searchAllFilters.getByRole("combobox");
await input.fill(text);
await expectAutocompletePopperOpen(searchAllFilters.page());
}

/**
* Returns a locator for the filter popover.
* @param page - Page.
* @returns A locator for the filter popover.
*/
function filterPopover(page: Page): Locator {
return page.getByTestId(TEST_IDS.FILTER_POPOVER);
}

/**
* Returns a regex matching a sidebar filter button, e.g. "Consent Code (5)".
* @param filterName - The name of the filter.
* @returns A RegExp matching the sidebar button text.
*/
function filterRegex(filterName: string): RegExp {
return new RegExp(escapeRegExp(filterName) + "\\s+\\(\\d+\\)\\s*");
}

/**
* Returns a locator for a filter tag (MuiChip) within the filters container.
* @param filters - The filters container locator.
* @param name - The filter option name to match.
* @returns A locator for the filter tag chip.
*/
function filterTag(filters: Locator, name: string): Locator {
return filters.locator(MUI_CLASSES.CHIP, { hasText: name });
}

/**
* Returns a locator for the first filter item in the open popover.
* @param page - Page.
* @returns A locator for the first filter item.
*/
function firstFilterItem(page: Page): Locator {
return filterPopover(page).getByTestId(TEST_IDS.FILTER_ITEM).first();
}

/**
* Returns the name of the first filter item in the open popover.
* @param page - Page.
* @returns The display name of the first option.
*/
async function getFirstOptionName(page: Page): Promise<string> {
return extractOptionName(firstFilterItem(page));
}

/**
* Returns a locator for a named filter item in the autocomplete popper.
* @param page - Page.
* @param optionName - The display name of the filter option.
* @returns A locator for the matching filter item.
*/
function namedFilterItem(page: Page, optionName: string): Locator {
return page
.locator(MUI_CLASSES.AUTOCOMPLETE_POPPER)
.getByTestId(TEST_IDS.FILTER_ITEM)
.filter({ hasText: RegExp(`^${escapeRegExp(optionName)}\\s*\\d+\\s*`) })
.first();
}

/**
* Opens a filter dropdown by clicking its sidebar button.
* Uses dispatchEvent because the filter menu sometimes intercepts regular clicks.
* @param filters - The filters container locator.
* @param filterName - The name of the sidebar filter to open.
*/
async function openFilterDropdown(
filters: Locator,
filterName: string
): Promise<void> {
await expectFilterPopoverClosed(filters.page());
const button = filters.getByText(filterRegex(filterName));
await expect(button).toBeVisible();
await button.dispatchEvent("click");
await expectFilterPopoverOpen(filters.page());
}

/**
* Returns a locator for a named filter item in the open filter popover.
* @param page - Page.
* @param optionName - The display name of the filter option.
* @returns A locator for the matching filter item.
*/
function namedPopoverFilterItem(page: Page, optionName: string): Locator {
return filterPopover(page)
.getByTestId(TEST_IDS.FILTER_ITEM)
.filter({ hasText: new RegExp(`^${escapeRegExp(optionName)}\\s*\\d+\\s*`) })
.first();
}

/**
* Opens a sidebar filter dropdown, selects its first option, and returns the
* option name. Waits for the item to be selected before returning.
* @param filters - The filters container locator.
* @param page - Page (needed for popover content).
* @param filterName - The name of the sidebar filter to open.
* @returns The display name of the selected option.
*/
async function selectFirstOption(
filters: Locator,
page: Page,
filterName: string
): Promise<string> {
await openFilterDropdown(filters, filterName);
const option = firstFilterItem(page);
const name = await extractOptionName(option);
await expectFilterItemNotSelected(option);
await option.click();
// Re-locate by name rather than position since the list may re-sort after
// selection, causing the positional `.first()` locator to resolve to a
// different (non-selected) element.
await expectFilterItemSelected(namedPopoverFilterItem(page, name));
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
await expectFilterPopoverClosed(page);
return name;
}
Loading
Loading