Skip to content

Commit

Permalink
test: added search filters tests (#35) (#52)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
jpaten authored Nov 8, 2024
1 parent 9b0b0a8 commit 56e349a
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 40 deletions.
25 changes: 25 additions & 0 deletions e2e/hprc-filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down Expand Up @@ -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);
});
12 changes: 12 additions & 0 deletions e2e/hprc-tabs.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 5 additions & 4 deletions e2e/test-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
227 changes: 191 additions & 36 deletions e2e/testFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ const getAllFilterNames = async (page: Page): Promise<string[]> => {
);
};

/**
* 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<string[]> => {
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.
Expand Down Expand Up @@ -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);
};

/**
Expand All @@ -165,31 +195,32 @@ export async function testFirstNFilterCounts(
n: number
): Promise<boolean> {
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<boolean> {
await page.goto(tab.url);
expectedCount: number
): Promise<boolean> => {
const elementsPerPageRegex = /^Results 1 - ([0-9]+) of [0-9]+/;
await expect(page.getByText(elementsPerPageRegex)).toBeVisible();
const elementsPerPageText = ((
Expand All @@ -202,36 +233,160 @@ 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<boolean> {
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");
// Exit the filter menu
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<FilterOptionNameAndLocator> => {
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<boolean> {
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;
}

Expand Down
2 changes: 2 additions & 0 deletions e2e/testInterfaces.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export interface TabDescription {
preselectedColumns: columnDescription[];
searchFiltersPlaceholderText: string;
selectableColumns: columnDescription[];
tabName: string;
url: string;
}

export interface HprcTabCollection {
alignments: TabDescription;
annotations: TabDescription;
assemblies: TabDescription;
rawSequencingData: TabDescription;
}
Expand Down

0 comments on commit 56e349a

Please sign in to comment.