Skip to content
Open
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
327 changes: 260 additions & 67 deletions e2e/anvil-catalog/anvilcatalog-filters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,267 @@
import { test } from "@playwright/test";
import { expect, Locator, Page, test } from "@playwright/test";
import {
testDeselectFiltersThroughSearchBar,
testSelectFiltersThroughSearchBar,
} from "../testFunctions";
import {
ANVIL_CATALOG_FILTERS,
ANVIL_CATALOG_TABS,
CONSENT_CODE_INDEX,
DBGAP_ID_INDEX,
TERRA_WORKSPACE_INDEX,
} from "./anvilcatalog-tabs";

const filterList = [CONSENT_CODE_INDEX, DBGAP_ID_INDEX, TERRA_WORKSPACE_INDEX];

test('Check that selecting filters through the "Search all Filters" textbox works correctly on the Consortia tab', async ({
page,
}) => {
await testSelectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.CONSORTIA,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
});
KEYBOARD_KEYS,
MUI_CLASSES,
TEST_IDS,
} from "../features/common/constants";
import { ANVIL_CATALOG_CATEGORY_NAMES } from "./constants";

test('Check that selecting filters through the "Search all Filters" textbox works correctly on the Studies tab', async ({
page,
}) => {
await testSelectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.STUDIES,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
});
const ENTITIES = [
{ name: "Consortia", url: "/data/consortia" },
{ name: "Studies", url: "/data/studies" },
{ name: "Workspaces", url: "/data/workspaces" },
];

test('Check that selecting filters through the "Search all Filters" textbox works correctly on the Workspaces tab', async ({
page,
}) => {
await testSelectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.WORKSPACES,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
});
const FACET_NAMES = [
ANVIL_CATALOG_CATEGORY_NAMES.CONSENT_CODE,
ANVIL_CATALOG_CATEGORY_NAMES.DBGAP_ID,
ANVIL_CATALOG_CATEGORY_NAMES.TERRA_WORKSPACE_NAME,
];

test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Consortia tab', async ({
page,
}) => {
await testDeselectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.CONSORTIA,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
});
test.describe("AnVIL Catalog filter search", () => {
for (const entity of ENTITIES) {
test.describe(`${entity.name} tab`, () => {
let filters: Locator;
let searchAllFilters: Locator;

test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Studies tab', async ({
page,
}) => {
await testDeselectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.STUDIES,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
});
test.beforeEach(async ({ page }) => {
await page.goto(entity.url);
filters = page.getByTestId(TEST_IDS.FILTERS);
searchAllFilters = page.getByTestId(TEST_IDS.SEARCH_ALL_FILTERS);
await filters.waitFor();
});

test("selects filters through search bar", async ({ page }) => {
for (const filterName of FACET_NAMES) {
// 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);

// 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);

test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Workspaces tab', async ({
page,
}) => {
await testDeselectFiltersThroughSearchBar(
page,
ANVIL_CATALOG_TABS.WORKSPACES,
filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i])
);
// Verify filter tag appeared
await expect(filterTag(filters, optionName)).toBeVisible();

// Clean up: remove filter tag
await filterTag(filters, optionName).dispatchEvent("click");
await expect(filterTag(filters, optionName)).not.toBeVisible();
}
});

test("deselects filters through search bar", async ({ page }) => {
for (const filterName of FACET_NAMES) {
// Select the first option through the dropdown
const optionName = await selectFirstOption(filters, page, filterName);
await expect(filterTag(filters, optionName)).toBeVisible();

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

// Verify filter tag disappeared
await expect(filterTag(filters, optionName)).not.toBeVisible();
}
});
});
}
});

/* ——————————————————————————— 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());
}

/**
* 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();
await expectFilterItemSelected(option);
await page.keyboard.press(KEYBOARD_KEYS.ESCAPE);
await expectFilterPopoverClosed(page);
return name;
}
19 changes: 0 additions & 19 deletions e2e/anvil-catalog/anvilcatalog-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,6 @@ import {
} from "./constants";

const ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT = "Search all filters...";
export const ANVIL_CATALOG_FILTERS = [
"Consent Code",
"Consortium",
"Data Type",
"dbGap Id",
"Disease (indication)",
"Study Design",
"Study",
"Terra Workspace Name",
];

export const CONSENT_CODE_INDEX = 0;
export const CONSORTIUM_INDEX = 1;
export const DATA_TYPE_INDEX = 2;
export const DBGAP_ID_INDEX = 3;
export const DISEASE_INDICATION_INDEX = 4;
export const STUDY_DESIGN_INDEX = 5;
export const STUDY_INDEX = 6;
export const TERRA_WORKSPACE_INDEX = 7;

export const ANVIL_CATALOG_TABS: AnvilCatalogTabCollection = {
CONSORTIA: {
Expand Down
6 changes: 6 additions & 0 deletions e2e/anvil-catalog/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export const ANVIL_CATALOG_CATEGORY_NAMES = {
CONSENT_CODE: "Consent Code",
DBGAP_ID: "dbGap Id",
TERRA_WORKSPACE_NAME: "Terra Workspace Name",
};

export const ANVIL_CATALOG_COLUMN_NAMES = {
CONSENT_CODE: "Consent Code",
CONSENT_CODES: "Consent Codes",
Expand Down
2 changes: 2 additions & 0 deletions e2e/features/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export const KEYBOARD_KEYS = {

export const MUI_CLASSES = {
ALERT: ".MuiAlert-root",
AUTOCOMPLETE_POPPER: ".MuiAutocomplete-popper",
BUTTON: ".MuiButton-root",
BUTTON_GROUP: ".MuiButtonGroup-root",
CHIP: ".MuiChip-root",
FORM_CONTROL: ".MuiFormControl-root",
ICON_BUTTON: ".MuiIconButton-root",
LIST: ".MuiList-root",
Expand Down
Loading
Loading