From 6eb42597a9ce1c633928fed61738b007d5bf8ea3 Mon Sep 17 00:00:00 2001 From: Jonah Paten Date: Mon, 16 Sep 2024 17:03:57 -0700 Subject: [PATCH] test: added anvil-catalog filter search test (#4125) (#4135) * test: added anvil-catalog filter search test (#4125) * test: bugfix in anvilcatalog filters test, updated testReadme (#4125) * test: added comments and tsdocs to filter search tests (#3342) * test: renamed files to match new test standard (#4135) --- .../anvilcatalog-filters.spec.ts | 74 +++++++ .../e2e/anvil-catalog/anvilcatalog-tabs.ts | 24 +++ explorer/e2e/anvil/anvil-tabs.ts | 7 + explorer/e2e/testFunctions.ts | 180 ++++++++++++++---- explorer/e2e/testInterfaces.ts | 1 + explorer/e2e/testReadme.md | 5 + 6 files changed, 253 insertions(+), 38 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..fad641cc9 --- /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 { + 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]) + ); +}); + +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]) + ); +}); + +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]) + ); +}); + +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('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('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]) + ); +}); diff --git a/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts b/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts index 6e3e035f3..8b11f2250 100644 --- a/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts +++ b/explorer/e2e/anvil-catalog/anvilcatalog-tabs.ts @@ -9,10 +9,32 @@ import { ANVIL_CATALOG_WORKSPACES_SELECTABLE_COLUMNS_BY_NAME, } 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: { emptyFirstColumn: false, preselectedColumns: ANVIL_CATALOG_CONSORTIA_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_CATALOG_CONSORTIA_SELECTABLE_COLUMNS_BY_NAME, tabName: "Consortia", url: "/data/consortia", @@ -20,6 +42,7 @@ export const ANVIL_CATALOG_TABS: AnvilCatalogTabCollection = { STUDIES: { emptyFirstColumn: false, preselectedColumns: ANVIL_CATALOG_STUDIES_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_CATALOG_STUDIES_SELECTABLE_COLUMNS_BY_NAME, tabName: "Studies", url: "/data/studies", @@ -27,6 +50,7 @@ export const ANVIL_CATALOG_TABS: AnvilCatalogTabCollection = { WORKSPACES: { emptyFirstColumn: false, preselectedColumns: ANVIL_CATALOG_WORKSPACES_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CATALOG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_CATALOG_WORKSPACES_SELECTABLE_COLUMNS_BY_NAME, tabName: "Workspaces", url: "/data/workspaces", diff --git a/explorer/e2e/anvil/anvil-tabs.ts b/explorer/e2e/anvil/anvil-tabs.ts index 58616689e..997d53d8e 100644 --- a/explorer/e2e/anvil/anvil-tabs.ts +++ b/explorer/e2e/anvil/anvil-tabs.ts @@ -43,11 +43,14 @@ 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 ANVIL_TABS: AnvilCMGTabCollection = { ACTIVITIES: { emptyFirstColumn: false, maxPages: 25, preselectedColumns: ANVIL_ACTIVITIES_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_ACTIVITIES_SELECTABLE_COLUMNS_BY_NAME, tabName: "Activities", url: "/activities", @@ -56,6 +59,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { emptyFirstColumn: false, maxPages: 25, preselectedColumns: ANVIL_BIOSAMPLES_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_BIOSAMPLES_SELECTABLE_COLUMNS_BY_NAME, tabName: "BioSamples", url: "/biosamples", @@ -130,6 +134,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { emptyFirstColumn: false, maxPages: 25, preselectedColumns: ANVIL_DATASETS_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_DATASETS_SELECTABLE_COLUMNS_BY_NAME, tabName: "Datasets", url: "/datasets", @@ -138,6 +143,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { emptyFirstColumn: false, maxPages: 25, preselectedColumns: ANVIL_DONORS_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_DONORS_SELECTABLE_COLUMNS_BY_NAME, tabName: "Donors", url: "/donors", @@ -146,6 +152,7 @@ export const ANVIL_TABS: AnvilCMGTabCollection = { emptyFirstColumn: true, maxPages: 25, preselectedColumns: ANVIL_FILES_PRESELECTED_COLUMNS_BY_NAME, + searchFiltersPlaceholderText: ANVIL_CMG_SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: ANVIL_DONORS_SELECTABLE_COLUMNS_BY_NAME, tabName: "Files", url: "/files", diff --git a/explorer/e2e/testFunctions.ts b/explorer/e2e/testFunctions.ts index 71b89fac2..977029c00 100644 --- a/explorer/e2e/testFunctions.ts +++ b/explorer/e2e/testFunctions.ts @@ -354,15 +354,6 @@ export async function testPreSelectedColumns( } } -/** - * Returns a string with special characters escaped - * @param string - the string to escape - * @returns - a string with special characters escaped - */ -export function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - /** * Returns a regex that matches the sidebar filter buttons * This is useful for selecting a filter from the sidebar @@ -405,13 +396,16 @@ 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 }); + // The Regex matches a filter name with a number after it, with potential whitespace before and after the number. + // This matches how the innerText in the filter options menu appears to Playwright. + return page.getByRole("button").filter({ + has: page.getByRole("checkbox"), + hasText: RegExp(`^${escapeRegExp(filterOptionName)}\\s*\\d+\\s*`), + }); }; /** @@ -419,13 +413,26 @@ 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 ( + firstFilterOptionLocator: Locator +): Promise => { + console.log(await firstFilterOptionLocator.innerText()); + // Filter options display as "[text]\n[number]" , sometimes with extra whitespace, so we split on newlines and take the first non-empty string + return ( + (await firstFilterOptionLocator.innerText()) + .split("\n") + .map((x) => x.trim()) + .find((x) => x.length > 0) ?? "" + ); +}; + /** * Cheks that selecting a specified filter is persistent across the tabs in tabOrder * @param page - a Playwright page object @@ -442,7 +449,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()) @@ -470,7 +477,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(); @@ -482,7 +489,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, }); @@ -512,8 +519,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 .reverse() @@ -526,7 +533,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(); @@ -543,6 +550,20 @@ export async function testFilterCounts( return true; } +const FILTER_CSS_SELECTOR = "#sidebar-positioner"; + +/** + * 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(FILTER_CSS_SELECTOR) + .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 @@ -560,21 +581,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"); @@ -583,7 +602,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(); } @@ -594,7 +613,7 @@ export async function testFilterTags( * those filters to become deselected * @param page - a Playwright page object * @param tab - the tab object to test on - * @param filterNames - the names of the fitlers to check + * @param filterNames - the names of the filters to check */ export async function testClearAll( page: Page, @@ -606,29 +625,25 @@ export async function testClearAll( // Select each filter and get the names of the actual filter text for (const filterName of filterNames) { 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(getFirstFilterOptionLocator(page)) ); await page.locator("body").click(); } // Click the "Clear All" button await page.getByText("Clear All").dispatchEvent("click"); for (const filterName of selectedFilterNamesList) { - await expect( - page.locator("#sidebar-positioner").getByText(filterName) - ).toHaveCount(0); + await expect(page.getByText(filterRegex(filterName))).toHaveCount(0); } // Ensure that the filters still show as unchecked 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(); @@ -636,6 +651,95 @@ export async function testClearAll( } } +/** + * 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 { + // Searches for regex special characters and adds backslashes in front of them to escape + 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) { + // Get the first filter option + await expect(page.getByText(filterRegex(filterName))).toBeVisible(); + await page.getByText(filterRegex(filterName)).dispatchEvent("click"); + const firstFilterOptionLocator = getFirstFilterOptionLocator(page); + const filterOptionName = await getFilterOptionName( + firstFilterOptionLocator + ); + await page.locator("body").click(); + // Search for the filter option + const searchFiltersInputLocator = page.getByPlaceholder( + tab.searchFiltersPlaceholderText, + { exact: true } + ); + await expect(searchFiltersInputLocator).toBeVisible(); + await searchFiltersInputLocator.fill(filterOptionName); + // Select a filter option with a matching name + await getNamedFilterOptionLocator(page, filterOptionName).first().click(); + await page.locator("body").click(); + const filterTagLocator = getFilterTagLocator(page, filterOptionName); + // Check the filter tag is selected and click it to reset the filter + 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); + for (const filterName of filterNames) { + // Select each filter option + await expect(page.getByText(filterRegex(filterName))).toBeVisible(); + await page.getByText(filterRegex(filterName)).dispatchEvent("click"); + const firstFilterOptionLocator = getFirstFilterOptionLocator(page); + const filterOptionName = await getFilterOptionName( + firstFilterOptionLocator + ); + await firstFilterOptionLocator.click(); + await page.locator("body").click(); + // Search for and check the selected filter + const searchFiltersInputLocator = page.getByPlaceholder( + tab.searchFiltersPlaceholderText, + { exact: true } + ); + await expect(searchFiltersInputLocator).toBeVisible(); + await searchFiltersInputLocator.fill(filterOptionName); + await getNamedFilterOptionLocator(page, filterOptionName) + .locator("input[type='checkbox']:checked") + .first() + .click(); + await page.locator("body").click(); + const filterTagLocator = getFilterTagLocator(page, filterOptionName); + await expect(filterTagLocator).not.toBeVisible(); + } +} + /** * Get the first link to a backpage with specified backpage access * @param page - a Playright page locator diff --git a/explorer/e2e/testInterfaces.ts b/explorer/e2e/testInterfaces.ts index 574fabb06..6be55ada1 100644 --- a/explorer/e2e/testInterfaces.ts +++ b/explorer/e2e/testInterfaces.ts @@ -9,6 +9,7 @@ export interface TabDescription { emptyFirstColumn: boolean; maxPages?: number; preselectedColumns: StringToColumnDescription; + searchFiltersPlaceholderText: string; selectableColumns: StringToColumnDescription; tabName: string; url: string; diff --git a/explorer/e2e/testReadme.md b/explorer/e2e/testReadme.md index aaf7cfea5..baf30e7eb 100644 --- a/explorer/e2e/testReadme.md +++ b/explorer/e2e/testReadme.md @@ -99,6 +99,11 @@ through the actions taken as part of the test and view the impact on the web pag - Runs on all tabs - Check that selecting all checkboxes in the Edit Columns menu adds the correct headers to the table - Only runs on the "Consortia" tab (other tabs do not have editable columns) +- Filters (`anvilcatalog-filters.spec.ts`) + - Search filters bar + - Check that filters can be selected through the search bar + - Check that filters can be deselected through the search bar + - Both tests run on all tabs - All tests rely on correct lists of tabs, columns, and filters in `anvilcatalog-tabs.ts` ### Candidate Additional Tests (anvil-cmg):