From ccf362a24b57a95ef306c65941c88fb9e422cc9a Mon Sep 17 00:00:00 2001 From: Jonah Paten Date: Thu, 29 Aug 2024 14:07:56 -0700 Subject: [PATCH] test: added anvil-catalog filter search test (#4125) --- .../anvilcatalog-filters.spec.ts | 74 ++++++++ .../e2e/anvil-catalog/anvilcatalog-tabs.ts | 24 +++ explorer/e2e/anvil/anvil-tabs.ts | 7 + explorer/e2e/testFunctions.ts | 161 +++++++++++++++--- explorer/e2e/testInterfaces.ts | 1 + explorer/playwright_anvil-catalog.config.ts | 1 + 6 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 explorer/e2e/anvil-catalog/anvilcatalog-filters.spec.ts diff --git a/explorer/e2e/anvil-catalog/anvilcatalog-filters.spec.ts b/explorer/e2e/anvil-catalog/anvilcatalog-filters.spec.ts new file mode 100644 index 000000000..4c7573f26 --- /dev/null +++ b/explorer/e2e/anvil-catalog/anvilcatalog-filters.spec.ts @@ -0,0 +1,74 @@ +import { test } from "@playwright/test"; +import { + testDeselectFiltersThroughSearchBar, + testSelectFiltersThroughSearchBar, +} from "../testFunctions"; +import { + anvilcatalogTabs, + ANVIL_CATALOG_FILTERS, + 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, + anvilcatalogTabs.consortia, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); + +test('Check that selecting filters through the "Search all Filters" textbox works correctly on the Studies tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar( + page, + anvilcatalogTabs.studies, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); + +test('Check that selecting filters through the "Search all Filters" textbox works correctly on the Workspaces tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar( + page, + anvilcatalogTabs.workspaces, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); + +test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Consortia tab', async ({ + page, +}) => { + await testDeselectFiltersThroughSearchBar( + page, + anvilcatalogTabs.consortia, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); + +test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Studies tab', async ({ + page, +}) => { + await testDeselectFiltersThroughSearchBar( + page, + anvilcatalogTabs.studies, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); + +test('Check that deselecting filters through the "Search all Filters" textbox works correctly on the Workspaces tab', async ({ + page, +}) => { + await testDeselectFiltersThroughSearchBar( + page, + anvilcatalogTabs.workspaces, + filterList.map((i: number) => ANVIL_CATALOG_FILTERS[i]) + ); +}); diff --git a/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts b/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts index b7e7f610c..07ee85b8d 100644 --- a/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts +++ b/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts @@ -2,6 +2,27 @@ import { AnvilCatalogTabCollection, TabDescription } from "../testInterfaces"; +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 anvilcatalogTabs: AnvilCatalogTabCollection = { consortia: { emptyFirstColumn: false, @@ -15,6 +36,7 @@ export const anvilcatalogTabs: AnvilCatalogTabCollection = { { name: "Participants", sortable: true }, { name: "Size (TB)", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [ { name: "Study", sortable: true }, { name: "Workspaces", sortable: true }, @@ -36,6 +58,7 @@ export const anvilcatalogTabs: AnvilCatalogTabCollection = { { name: "Participants", sortable: true }, { name: "Size (TB)", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Studies", url: "/data/studies", @@ -54,6 +77,7 @@ export const anvilcatalogTabs: AnvilCatalogTabCollection = { { name: "Participants", sortable: true }, { name: "Size (TB)", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Workspaces", url: "/data/workspaces", diff --git a/explorer/e2e/anvil/anvil-tabs.ts b/explorer/e2e/anvil/anvil-tabs.ts index 800190506..955feb528 100644 --- a/explorer/e2e/anvil/anvil-tabs.ts +++ b/explorer/e2e/anvil/anvil-tabs.ts @@ -30,6 +30,8 @@ export const ORGANISM_TYPE_INDEX = 8; export const PHENOTYPIC_SEX_INDEX = 9; export const REPORTED_ETHNICITY_INDEX = 10; +const ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT = "Search all filters..."; + export const anvilTabs: AnvilCMGTabCollection = { activities: { emptyFirstColumn: false, @@ -42,6 +44,7 @@ export const anvilTabs: AnvilCMGTabCollection = { { name: "Organism Type", sortable: true }, { name: "Dataset", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [ { name: "Phenotypic Sex", sortable: true }, { name: "Reported Ethnicity", sortable: true }, @@ -61,6 +64,7 @@ export const anvilTabs: AnvilCMGTabCollection = { { name: "Diagnosis", sortable: true }, { name: "Dataset", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [ { name: "Phenotypic Sex", sortable: true }, { name: "Reported Ethnicity", sortable: true }, @@ -80,6 +84,7 @@ export const anvilTabs: AnvilCMGTabCollection = { { name: "Diagnosis", sortable: true }, { name: "Data Modality", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [ { name: "Phenotypic Sex", sortable: true }, { name: "Reported Ethnicity", sortable: true }, @@ -98,6 +103,7 @@ export const anvilTabs: AnvilCMGTabCollection = { { name: "Diagnosis", sortable: true }, { name: "Dataset", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Donors", url: "/donors", @@ -114,6 +120,7 @@ export const anvilTabs: AnvilCMGTabCollection = { { name: "Organism Type", sortable: true }, { name: "Dataset", sortable: true }, ], + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [ { name: "Phenotypic Sex", sortable: true }, { name: "Reported Ethnicity", sortable: true }, diff --git a/explorer/e2e/testFunctions.ts b/explorer/e2e/testFunctions.ts index ef4f9021c..7187084d8 100644 --- a/explorer/e2e/testFunctions.ts +++ b/explorer/e2e/testFunctions.ts @@ -266,7 +266,7 @@ export async function testPreSelectedColumns( * @returns a regular expression matching "[filterName] ([n])" */ export const filterRegex = (filterName: string): RegExp => - new RegExp(filterName + "\\s+\\([0-9]+\\)\\s*"); + new RegExp(escapeRegExp(filterName) + "\\s+\\([0-9]+\\)\\s*"); /** * Checks that each filter specified in filterNames is visible and can be @@ -301,13 +301,14 @@ export async function testFilterPresence( * @param filterOptionName - the name of the filter option * @returns a Playwright locator to the filter button */ -export const getNamedFilterButtonLocator = ( +export const getNamedFilterOptionLocator = ( page: Page, filterOptionName: string ): Locator => { - return page - .getByRole("button") - .filter({ has: page.getByRole("checkbox"), hasText: filterOptionName }); + return page.getByRole("button").filter({ + has: page.getByRole("checkbox"), + hasText: RegExp(`^${escapeRegExp(filterOptionName)}\\s*\\d+\\s*`), + }); }; /** @@ -315,13 +316,24 @@ export const getNamedFilterButtonLocator = ( * @param page - a Playwright page object * @returns a Playwright locator to the filter button */ -export const getFirstFilterButtonLocator = (page: Page): Locator => { +export const getFirstFilterOptionLocator = (page: Page): Locator => { return page .getByRole("button") .filter({ has: page.getByRole("checkbox") }) .first(); }; +export const getFilterOptionName = async ( + page: Page, + firstFilterOptionLocator: Locator +): Promise => { + return ( + (await firstFilterOptionLocator.innerText()) + .split("\n") + .find((x) => x.length > 0) ?? "" + ); +}; + /** * Cheks that selecting a specified filter is persistent across the tabs in tabOrder * @param page - a Playwright page object @@ -338,7 +350,7 @@ export async function testFilterPersistence( await page.goto(tabOrder[0].url); // Select the first checkbox on the test filter await page.getByText(filterRegex(testFilterName)).click(); - const filterToSelectLocator = await getFirstFilterButtonLocator(page); + const filterToSelectLocator = await getFirstFilterOptionLocator(page); await expect(filterToSelectLocator.getByRole("checkbox")).not.toBeChecked(); await filterToSelectLocator.getByRole("checkbox").click(); const filterNameMatch = (await filterToSelectLocator.innerText()) @@ -364,7 +376,7 @@ export async function testFilterPersistence( await expect(page.getByText(filterRegex(testFilterName))).toBeVisible(); await page.getByText(filterRegex(testFilterName)).dispatchEvent("click"); await page.waitForLoadState("load"); - const previouslySelected = getNamedFilterButtonLocator(page, filterName); + const previouslySelected = getNamedFilterOptionLocator(page, filterName); await expect(previouslySelected.getByRole("checkbox")).toBeChecked(); await page.waitForLoadState("load"); await page.locator("body").click(); @@ -376,7 +388,7 @@ export async function testFilterPersistence( .dispatchEvent("click"); await expect(getFirstRowNthColumnCellLocator(page, 0)).toBeVisible(); await page.getByText(filterRegex(testFilterName)).dispatchEvent("click"); - const previouslySelected = getFirstFilterButtonLocator(page); + const previouslySelected = getFirstFilterOptionLocator(page); await expect(previouslySelected).toContainText(filterName, { useInnerText: true, }); @@ -406,8 +418,8 @@ export async function testFilterCounts( await page.getByText(filterRegex(filterName)).dispatchEvent("click"); // Get the number associated with the first filter button, and select it await page.waitForLoadState("load"); - const filterButton = getFirstFilterButtonLocator(page); - const filterNumbers = (await filterButton.innerText()).split("\n"); + const firstFilterOption = getFirstFilterOptionLocator(page); + const filterNumbers = (await firstFilterOption.innerText()).split("\n"); const filterNumber = filterNumbers.map((x) => Number(x)).find((x) => !isNaN(x) && x !== 0) ?? -1; @@ -416,7 +428,7 @@ export async function testFilterCounts( return false; } // Check the filter - await filterButton.getByRole("checkbox").dispatchEvent("click"); + await firstFilterOption.getByRole("checkbox").dispatchEvent("click"); await page.waitForLoadState("load"); // Exit the filter menu await page.locator("body").click(); @@ -431,6 +443,18 @@ export async function testFilterCounts( return true; } +/** + * Get a locator for a named filter tag + * @param page - a Playwright page object + * @param filterTagName - the name of the filter tag to search for + * @returns - a locator for the named filter tag + */ +const getFilterTagLocator = (page: Page, filterTagName: string): Locator => { + return page + .locator("#sidebar-positioner") + .getByText(filterTagName, { exact: true }); +}; + /** * Check that the filter tabs appear when a filter is selected and that clicking * them causes the filter to be deselected @@ -448,21 +472,19 @@ export async function testFilterTags( // Select a filter await page.getByText(filterRegex(filterName)).dispatchEvent("click"); await page.waitForLoadState("load"); - const firstFilterButtonLocator = getFirstFilterButtonLocator(page); + const firstFilterOptionLocator = getFirstFilterOptionLocator(page); // Get the name of the selected filter - const firstFilterName = - (await firstFilterButtonLocator.innerText()) + const firstFilterOptionName = + (await firstFilterOptionLocator.innerText()) .split("\n") .find((x) => x.length > 0) ?? ""; // Click the selected filter and exit the filter menu - await firstFilterButtonLocator.getByRole("checkbox").click(); + await firstFilterOptionLocator.getByRole("checkbox").click(); await page.waitForLoadState("load"); await page.locator("body").click(); await expect(page.getByRole("checkbox")).toHaveCount(0); // Click the filter tag - const filterTagLocator = page - .locator("#sidebar-positioner") - .getByText(firstFilterName); + const filterTagLocator = getFilterTagLocator(page, firstFilterOptionName); await expect(filterTagLocator).toBeVisible(); await filterTagLocator.scrollIntoViewIfNeeded(); await filterTagLocator.dispatchEvent("click"); @@ -471,7 +493,7 @@ export async function testFilterTags( // Expect the filter to be deselected in the filter menu await page.getByText(filterRegex(filterName)).dispatchEvent("click"); await expect( - firstFilterButtonLocator.getByRole("checkbox") + firstFilterOptionLocator.getByRole("checkbox") ).not.toBeChecked(); await page.locator("body").click(); } @@ -494,14 +516,12 @@ export async function testClearAll( for (const filterName of filterNames) { // Select the passed filter names await page.getByText(filterRegex(filterName)).dispatchEvent("click"); - await getFirstFilterButtonLocator(page).getByRole("checkbox").click(); + await getFirstFilterOptionLocator(page).getByRole("checkbox").click(); await expect( - getFirstFilterButtonLocator(page).getByRole("checkbox") + getFirstFilterOptionLocator(page).getByRole("checkbox") ).toBeChecked(); selectedFilterNamesList.push( - (await getFirstFilterButtonLocator(page).innerText()) - .split("\n") - .find((x) => x.length > 0) ?? "" + await getFilterOptionName(page, getFirstFilterOptionLocator(page)) ); await page.locator("body").click(); } @@ -515,11 +535,100 @@ export async function testClearAll( for (let i = 0; i < filterNames.length; i++) { await page.getByText(filterRegex(filterNames[i])).dispatchEvent("click"); await expect( - getNamedFilterButtonLocator(page, selectedFilterNamesList[i]).getByRole( + getNamedFilterOptionLocator(page, selectedFilterNamesList[i]).getByRole( "checkbox" ) ).not.toBeChecked(); await page.locator("body").click(); } } + +/** + * Escape a string so it can safely be used in a regexp + * @param string - the string to escape + * @returns - A string that has all Regexp special characters escaped + */ +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * Run a test that gets the first filter option of each of the filters specified in + * filterNames, then attempts to select each through the filter search bar. + * @param page - a Playwright page object + * @param tab - the Tab object to run the test on + * @param filterNames - an array of potential filter names on the selected tab + */ +export async function testSelectFiltersThroughSearchBar( + page: Page, + tab: TabDescription, + filterNames: string[] +): Promise { + await page.goto(tab.url); + for (const filterName of filterNames) { + await expect(page.getByText(filterRegex(filterName))).toBeVisible(); + await page.getByText(filterRegex(filterName)).dispatchEvent("click"); + const firstFilterOptionLocator = getFirstFilterOptionLocator(page); + const filterOptionName = await getFilterOptionName( + page, + firstFilterOptionLocator + ); + await page.locator("body").click(); + + const searchFiltersInputLocator = page.getByPlaceholder( + tab.searchFiltersPlaceholderText, + { exact: true } + ); + await expect(searchFiltersInputLocator).toBeVisible(); + await searchFiltersInputLocator.fill(filterOptionName); + await getNamedFilterOptionLocator(page, filterOptionName).first().click(); + await page.locator("body").click(); + const filterTagLocator = getFilterTagLocator(page, filterOptionName); + await expect(filterTagLocator).toBeVisible(); + await filterTagLocator.dispatchEvent("click"); + } +} + +/** + * Run a test that selects the first filter option of each of the filters specified in + * filterNames, then attempts to deselect each through the filter search bar. + * @param page - a Playwright page object + * @param tab - the Tab object to run the test on + * @param filterNames - an array of potential filter names on the selected tab + */ +export async function testDeselectFiltersThroughSearchBar( + page: Page, + tab: TabDescription, + filterNames: string[] +): Promise { + await page.goto(tab.url); + const filterOptionNames: string[] = []; + for (const filterName of filterNames) { + await expect(page.getByText(filterRegex(filterName))).toBeVisible(); + await page.getByText(filterRegex(filterName)).dispatchEvent("click"); + const firstFilterOptionLocator = getFirstFilterOptionLocator(page); + const filterOptionName = await getFilterOptionName( + page, + firstFilterOptionLocator + ); + filterOptionNames.push(filterOptionName); + await firstFilterOptionLocator.click(); + await page.locator("body").click(); + } + for (let i = 0; i < filterOptionNames.length; i++) { + const searchFiltersInputLocator = page.getByPlaceholder( + tab.searchFiltersPlaceholderText, + { exact: true } + ); + await expect(searchFiltersInputLocator).toBeVisible(); + await searchFiltersInputLocator.fill(filterOptionNames[i]); + await getNamedFilterOptionLocator(page, filterOptionNames[i]) + .locator("input[type='checkbox']:checked") + .first() + .click(); + await page.locator("body").click(); + const filterTagLocator = getFilterTagLocator(page, filterOptionNames[i]); + await expect(filterTagLocator).not.toBeVisible(); + } +} /* eslint-enable sonarjs/no-duplicate-string -- Checking duplicate strings again*/ diff --git a/explorer/e2e/testInterfaces.ts b/explorer/e2e/testInterfaces.ts index ee5d786c3..5839aa7b0 100644 --- a/explorer/e2e/testInterfaces.ts +++ b/explorer/e2e/testInterfaces.ts @@ -2,6 +2,7 @@ export interface TabDescription { emptyFirstColumn: boolean; maxPages?: number; preselectedColumns: columnDescription[]; + searchFiltersPlaceholderText: string; selectableColumns: columnDescription[]; tabName: string; url: string; diff --git a/explorer/playwright_anvil-catalog.config.ts b/explorer/playwright_anvil-catalog.config.ts index 19bf82642..96f67c9da 100644 --- a/explorer/playwright_anvil-catalog.config.ts +++ b/explorer/playwright_anvil-catalog.config.ts @@ -22,6 +22,7 @@ const config: PlaywrightTestConfig = { ], testDir: "e2e", testMatch: /.*\/(anvil-catalog)\/.*\.spec\.ts/, + timeout: 60 * 1000, use: { baseURL: "http://localhost:3000/", screenshot: "only-on-failure",