From 56e349a6e4a17bd461f6319bbf35b23ce7a91f8c Mon Sep 17 00:00:00 2001 From: Jonah Paten Date: Fri, 8 Nov 2024 11:50:49 -0800 Subject: [PATCH] test: added search filters tests (#35) (#52) * test: added search filters tests (#35) * test: code style fix (#35) * test: fixed potential flakiness (#35) * test: updated readme (#35) * test: simplified filter search tests (#35) --- e2e/hprc-filters.spec.ts | 25 +++++ e2e/hprc-tabs.ts | 12 +++ e2e/test-readme.md | 9 +- e2e/testFunctions.ts | 227 ++++++++++++++++++++++++++++++++------- e2e/testInterfaces.ts | 2 + 5 files changed, 235 insertions(+), 40 deletions(-) diff --git a/e2e/hprc-filters.spec.ts b/e2e/hprc-filters.spec.ts index aa32ef4..8fdfef4 100644 --- a/e2e/hprc-filters.spec.ts +++ b/e2e/hprc-filters.spec.ts @@ -3,6 +3,7 @@ import { HPRC_TABS } from "./hprc-tabs"; import { testAllFiltersPresence, testFirstNFilterCounts, + testSelectFiltersThroughSearchBar, } from "./testFunctions"; test("Expect at least one filter to exist on the Raw Sequencing Data tab and for all filters to function", async ({ @@ -53,3 +54,27 @@ test("Expect filter counts to update for the first five on the Alignments tab", test.fail(); } }); + +test('Check that selecting the first filter through the "Search all Filters" textbox works correctly on the Raw Sequencing Data tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar(page, HPRC_TABS.rawSequencingData); +}); + +test('Check that selecting the first filter through the "Search all Filters" textbox works correctly on the Assemblies tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar(page, HPRC_TABS.assemblies); +}); + +test('Check that selecting the first filter through the "Search all Filters" textbox works correctly on the Annotations tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar(page, HPRC_TABS.annotations); +}); + +test('Check that selecting the first filter through the "Search all Filters" textbox works correctly on the Alignments tab', async ({ + page, +}) => { + await testSelectFiltersThroughSearchBar(page, HPRC_TABS.alignments); +}); diff --git a/e2e/hprc-tabs.ts b/e2e/hprc-tabs.ts index 424979e..19a30f6 100644 --- a/e2e/hprc-tabs.ts +++ b/e2e/hprc-tabs.ts @@ -1,20 +1,32 @@ import { HprcTabCollection } from "./testInterfaces"; +const SEARCH_FILTERS_PLACEHOLDER_TEXT = "Search all filters..."; + export const HPRC_TABS: HprcTabCollection = { alignments: { preselectedColumns: [], + searchFiltersPlaceholderText: SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Alignments", url: "/alignments", }, + annotations: { + preselectedColumns: [], + searchFiltersPlaceholderText: SEARCH_FILTERS_PLACEHOLDER_TEXT, + selectableColumns: [], + tabName: "Annotations", + url: "/annotations", + }, assemblies: { preselectedColumns: [], + searchFiltersPlaceholderText: SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Assemblies", url: "/assemblies", }, rawSequencingData: { preselectedColumns: [], + searchFiltersPlaceholderText: SEARCH_FILTERS_PLACEHOLDER_TEXT, selectableColumns: [], tabName: "Sequencing Data", url: "/raw-sequencing-data", diff --git a/e2e/test-readme.md b/e2e/test-readme.md index e259a7c..503616c 100644 --- a/e2e/test-readme.md +++ b/e2e/test-readme.md @@ -38,21 +38,22 @@ through the actions taken as part of the test and view the impact on the web pag - Check that all text that matches a filter regex is clickable, shows a filter menu with checkboxes when clicked, and that the filter menu disapperas when the center of the page is clicked - Uses the regex "^(.+)\s+\([0-9]+\)\s\*" to match all filter buttons - Once the list of filters is finalized, this should be converted to using a constant list of filters - - Runs on all three tabs + - Runs on all three tabs except Annotations - Check that the filter counts in the filter menus match the resulting row counts for the main table for the first three filters on each tab - Once the list of filters is finalized, this should be converted to using a constant list of filters + - Check that the filter search bar can be used to select and deselect tests (runs on all four tabs) - Sort (`hprc-sort.spec.ts`) - Check that clicking the table header of the first row switches the first and last rows in that row - Does not check that any actual sorting occurs, only that the first and last rows are switched - - Runs on all three tables + - Runs on all three tables except Annotations - Should be expanded to run on all sortable columns once column names are finalized - Navigation - Check that all tabs appear on each tab page - - Runs on all tabs + - Runs on all tabs except Annotations - Cannot tell what tabs are selected because the aria-selected value is not set for the tab buttons - `hprc-urls.spec.ts` - Check that the data table appears on each tab and that the first cell of the first column is visible - - Runs on all tabs + - Runs on all tabs except Annotations - `hprc-table.spec.ts` - Check that `/` redirects to `/raw-sequencing-data` (`smoke-test.spec.ts`) - All tests rely on correct lists of tabs in `hprc-tabs.ts` diff --git a/e2e/testFunctions.ts b/e2e/testFunctions.ts index c088b2b..f6fe23d 100644 --- a/e2e/testFunctions.ts +++ b/e2e/testFunctions.ts @@ -78,6 +78,26 @@ const getAllFilterNames = async (page: Page): Promise => { ); }; +/** + * Get the names of the first n filters on the page + * @param page - a Playwright page object + * @param n - the number of filters to test + * @returns - true if the test passes and false if the test should fail + */ +const getFirstNFilterNames = async ( + page: Page, + n: number +): Promise => { + const allFilterNames = await getAllFilterNames(page); + if (allFilterNames.length < n) { + console.log( + `There are only ${allFilterNames.length} filters, which is fewer than the ${n} specified for this test` + ); + return [""]; + } + return allFilterNames.slice(0, n); +}; + /** * Test that all text that looks like a filter button is clickable and opens * a filter menu with at least one checkbox. @@ -139,15 +159,25 @@ export const getNamedFilterButtonLocator = ( }; /** - * Get a locator for the first filter option on the page. + * Get a locator for the nth filter option on the page. * @param page - a Playwright page object + * @param n - the index of the filter option to get * @returns - a Playwright locator object for the first filter option on the page */ -export const getFirstFilterButtonLocator = (page: Page): Locator => { +const getNthFilterOptionLocator = (page: Page, n: number): Locator => { return page .getByRole("button") .filter({ has: page.getByRole("checkbox") }) - .first(); + .nth(n); +}; + +/** + * Get a locator for the first filter option on the page. + * @param page - a Playwright page object + * @returns - a Playwright locator object for the first filter option on the page + */ +export const getFirstFilterOptionLocator = (page: Page): Locator => { + return getNthFilterOptionLocator(page, 0); }; /** @@ -165,31 +195,32 @@ export async function testFirstNFilterCounts( n: number ): Promise { await page.goto(tab.url); - const allFilterNames = await getAllFilterNames(page); - if (allFilterNames.length < n) { - console.log( - `There are only ${allFilterNames.length} filters, which is fewer than the ${n} specified for this test` - ); + const firstNFilterNames = await getFirstNFilterNames(page, n); + if (firstNFilterNames.length < n) { return false; } - const firstNFilterNames = allFilterNames.slice(0, n); return await testFilterCounts(page, tab, firstNFilterNames); } /** - * Test that the counts associated with an array of filter names are reflected - * in the table - * @param page - a Playwright page object - * @param tab - the tab object to test - * @param filterNames - the names of the filters to select, in order - * @returns false if the test should fail and true if the test should pass + * Get the count associated with a filter option + * @param filterText - The text resulting from the innerText of a filter option + * @returns - the number associated with the filter option */ -export async function testFilterCounts( +const getFilterNumberFromText = (filterText: string): number => { + const filterNumbers = filterText.split("\n"); + return ( + filterNumbers + .reverse() + .map((x) => Number(x)) + .find((x) => !isNaN(x) && x !== 0) ?? -1 + ); +}; + +const verifyFilterCount = async ( page: Page, - tab: TabDescription, - filterNames: string[] -): Promise { - await page.goto(tab.url); + expectedCount: number +): Promise => { const elementsPerPageRegex = /^Results 1 - ([0-9]+) of [0-9]+/; await expect(page.getByText(elementsPerPageRegex)).toBeVisible(); const elementsPerPageText = (( @@ -202,23 +233,38 @@ export async function testFilterCounts( ); return false; } + const firstNumber = + expectedCount <= elementsPerPage ? expectedCount : elementsPerPage; + await expect( + page.getByText("Results 1 - " + firstNumber + " of " + expectedCount) + ).toBeVisible(); + return true; +}; + +/** + * Test that the counts associated with an array of filter names are reflected + * in the table + * @param page - a Playwright page object + * @param tab - the tab object to test + * @param filterNames - the names of the filters to select, in order + * @returns false if the test should fail and true if the test should pass + */ +export async function testFilterCounts( + page: Page, + tab: TabDescription, + filterNames: string[] +): Promise { + await page.goto(tab.url); // For each arbitrarily selected filter for (const filterName of filterNames) { // Select the filter 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 filterNumber = - filterNumbers - .reverse() - .map((x) => Number(x)) - .find((x) => !isNaN(x) && x !== 0) ?? -1; - if (filterNumber < 0) { - console.log(filterNumbers.map((x) => Number(x))); - return false; - } + const filterButton = getFirstFilterOptionLocator(page); + const filterNumber = getFilterNumberFromText( + await filterButton.innerText() + ); // Check the filter await filterButton.getByRole("checkbox").dispatchEvent("click"); await page.waitForLoadState("load"); @@ -226,12 +272,121 @@ export async function testFilterCounts( await page.locator("body").click(); await expect(page.getByRole("checkbox")).toHaveCount(0); // Expect the displayed count of elements to be 0 - const firstNumber = - filterNumber <= elementsPerPage ? filterNumber : elementsPerPage; - await expect( - page.getByText("Results 1 - " + firstNumber + " of " + filterNumber) - ).toBeVisible(); + const filterCountPassed = await verifyFilterCount(page, filterNumber); + if (!filterCountPassed) { + return false; + } + } + return true; +} + +/** + * Get a locator for the specified filter option. Requires a filter menu to be open + * @param page - a Playwright page object + * @param filterOptionName - the name of the filter option + * @returns a Playwright locator to the filter button + */ +export const getNamedFilterOptionLocator = ( + page: Page, + filterOptionName: string +): Locator => { + // 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*`), + }); +}; + +interface FilterOptionNameAndLocator { + locator: Locator; + name: string; +} + +const MAX_FILTER_OPTIONS_TO_CHECK = 10; + +/** + * Gets the name of the filter option associated with a locator + * @param page - a Playwright Page object, on which a filter must be currently selected + * @returns the innerText of the first nonempty filter option as a promise + */ +const getFirstNonEmptyFilterOptionNameAndIndex = async ( + page: Page +): Promise => { + let filterToSelect = ""; + let filterOptionLocator = undefined; + let i = 0; + while (filterToSelect === "" && i < MAX_FILTER_OPTIONS_TO_CHECK) { + // Filter options display as "[text]\n[number]" , sometimes with extra whitespace, so we want the string before the newline + const filterOptionRegex = /^(.*)\n+([0-9]+)\s*$/; + filterOptionLocator = getNthFilterOptionLocator(page, i); + filterToSelect = ((await filterOptionLocator.innerText()) + .trim() + .match(filterOptionRegex) ?? ["", ""])[1]; + i += 1; + } + if (filterOptionLocator === undefined) { + throw new Error( + "No locator found within the maximum number of filter options" + ); + } + return { locator: filterOptionLocator, name: filterToSelect }; +}; + +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 }); +}; + +/** + * Run a test that selects a filter option through the search bar and checks that it becomes selected + * @param page - a Playwright page object + * @param tab - the Tab object to run the test on + * @returns - true if the test passes and false if the test should fail + */ +export async function testSelectFiltersThroughSearchBar( + page: Page, + tab: TabDescription +): Promise { + await page.goto(tab.url); + // Select the filter search bar using placeholder text + const searchFiltersInputLocator = page.getByPlaceholder( + tab.searchFiltersPlaceholderText, + { exact: true } + ); + await expect(searchFiltersInputLocator).toBeVisible(); + await searchFiltersInputLocator.click(); + // Select the first filter with associated text + const firstFilterWithTextNameAndLocator = + await getFirstNonEmptyFilterOptionNameAndIndex(page); + const filterCount = getFilterNumberFromText( + await firstFilterWithTextNameAndLocator.locator.innerText() + ); + await firstFilterWithTextNameAndLocator.locator.click(); + await page.locator("body").click(); + const filterTagLocator = getFilterTagLocator( + page, + firstFilterWithTextNameAndLocator.name + ); + // Check the filter tag is selected + await expect(filterTagLocator).toBeVisible(); + // Check that the filter counts are equal to the number associated with the selected filter + const filterCountSuccess = await verifyFilterCount(page, filterCount); + if (!filterCountSuccess) { + return false; } + // Click to remove the filter tag + await filterTagLocator.dispatchEvent("click"); + await expect(filterTagLocator).not.toBeVisible(); return true; } diff --git a/e2e/testInterfaces.ts b/e2e/testInterfaces.ts index 3a4a40a..3c12f26 100644 --- a/e2e/testInterfaces.ts +++ b/e2e/testInterfaces.ts @@ -1,5 +1,6 @@ export interface TabDescription { preselectedColumns: columnDescription[]; + searchFiltersPlaceholderText: string; selectableColumns: columnDescription[]; tabName: string; url: string; @@ -7,6 +8,7 @@ export interface TabDescription { export interface HprcTabCollection { alignments: TabDescription; + annotations: TabDescription; assemblies: TabDescription; rawSequencingData: TabDescription; }