Skip to content

Commit

Permalink
[Issue #2144] e2e tests for clear/select all filters and fix nested f…
Browse files Browse the repository at this point in the history
…ilter bug (#3319)

* adds e2e tests for "select all" and "clear all" filter functionality, including for agencies / nested filters
* fixes bugs with nested filters that showed incorrect selected filter counts and did not correctly clear selected filters
  • Loading branch information
doug-s-nava authored Dec 30, 2024
1 parent 7d5d4f8 commit 257b7cf
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { camelCase } from "lodash";
import { QueryContext } from "src/app/[locale]/search/QueryProvider";
import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater";
import { QueryParamKey } from "src/types/search/searchResponseTypes";
Expand Down Expand Up @@ -43,20 +44,42 @@ export interface FilterOptionWithChildren {
children: FilterOption[];
}

const isSectionAllSelected = (
allSelected: Set<string>,
query: Set<string>,
): boolean => {
return areSetsEqual(allSelected, query);
};

const isSectionNoneSelected = (query: Set<string>): boolean => {
return query.size === 0;
};

const areSetsEqual = (a: Set<string>, b: Set<string>) =>
a.size === b.size && [...a].every((value) => b.has(value));

export function SearchFilterAccordion({
filterOptions,
title,
queryParamKey,
query,
}: SearchFilterAccordionProps) {
const { queryTerm } = useContext(QueryContext);
const { updateQueryParams } = useSearchParamUpdater();
const { updateQueryParams, searchParams } = useSearchParamUpdater();

const totalCheckedCount = query.size;
// These are all of the available selectedable options.
const allOptionValues = filterOptions.map((options) => options.value);
// This is the setting if all are selected.
// all top level selectable filter options
const allOptionValues = filterOptions.reduce((values: string[], option) => {
if (option.children) {
return values;
}
values.push(option.value);
return values;
}, []);

const allSelected = new Set(allOptionValues);

// SPLIT ME INTO MY OWN COMPONENT
const getAccordionTitle = () => (
<>
{title}
Expand All @@ -68,29 +91,25 @@ export function SearchFilterAccordion({
</>
);

const toggleSelectAll = (all: boolean, allSelected: Set<string>): void => {
if (all) {
updateQueryParams(allSelected, queryParamKey, queryTerm);
// need to add any existing relevant search params to the passed in set
const toggleSelectAll = (all: boolean, newSelections?: Set<string>): void => {
if (all && newSelections) {
// get existing current selected options for this accordion from url
const currentSelections = new Set(
searchParams.get(camelCase(title))?.split(","),
);
// add existing to newly selected section
const sectionPlusCurrent = new Set([
...currentSelections,
...newSelections,
]);
updateQueryParams(sectionPlusCurrent, queryParamKey, queryTerm);
} else {
const noneSelected = new Set<string>();
updateQueryParams(noneSelected, queryParamKey, queryTerm);
const clearedSelections = newSelections || new Set<string>();
updateQueryParams(clearedSelections, queryParamKey, queryTerm);
}
};

const isSectionAllSelected = (
allSelected: Set<string>,
query: Set<string>,
): boolean => {
return areSetsEqual(allSelected, query);
};

const isSectionNoneSelected = (query: Set<string>): boolean => {
return query.size === 0;
};

const areSetsEqual = (a: Set<string>, b: Set<string>) =>
a.size === b.size && [...a].every((value) => b.has(value));

const toggleOptionChecked = (value: string, isChecked: boolean) => {
const updated = new Set(query);
isChecked ? updated.add(value) : updated.delete(value);
Expand All @@ -99,11 +118,12 @@ export function SearchFilterAccordion({

const isExpanded = !!query.size;

// SPLIT ME INTO MY OWN COMPONENT
const getAccordionContent = () => (
<>
<SearchFilterToggleAll
onSelectAll={() => toggleSelectAll(true, allSelected)}
onClearAll={() => toggleSelectAll(false, allSelected)}
onClearAll={() => toggleSelectAll(false)}
isAllSelected={isSectionAllSelected(allSelected, query)}
isNoneSelected={isSectionNoneSelected(query)}
/>
Expand Down Expand Up @@ -138,12 +158,13 @@ export function SearchFilterAccordion({
</>
);

// MEMOIZE ME
const accordionOptions: AccordionItemProps[] = [
{
title: getAccordionTitle(),
content: getAccordionContent(),
expanded: isExpanded,
id: `funding-instrument-filter-${queryParamKey}`,
id: `opportunity-filter-${queryParamKey}`,
headingLevel: "h2",
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use client";

import { useState } from "react";
import { camelCase } from "lodash";

import { useSearchParams } from "next/navigation";
import { useCallback, useMemo, useState } from "react";

import { FilterOptionWithChildren } from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import SearchFilterCheckbox from "src/components/search/SearchFilterAccordion/SearchFilterCheckbox";
Expand All @@ -11,7 +14,7 @@ import SearchFilterToggleAll from "src/components/search/SearchFilterAccordion/S
interface SearchFilterSectionProps {
option: FilterOptionWithChildren;
updateCheckedOption: (optionId: string, isChecked: boolean) => void;
toggleSelectAll: (all: boolean, allSelected: Set<string>) => void;
toggleSelectAll: (all: boolean, allSelected?: Set<string>) => void;
accordionTitle: string;
isSectionAllSelected: (
allSelected: Set<string>,
Expand All @@ -33,6 +36,7 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
value,
}) => {
const [childrenVisible, setChildrenVisible] = useState<boolean>(false);
const searchParams = useSearchParams();

const sectionQuery = new Set<string>();
query.forEach((queryValue) => {
Expand All @@ -42,16 +46,26 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
sectionQuery.add(queryValue);
}
});
const allSectionOptionValues = option.children.map(
(options) => options.value,
const allSectionOptions = useMemo(
() => new Set(option.children.map((options) => options.value)),
[option],
);
const sectionAllSelected = new Set(allSectionOptionValues);

const sectionCount = sectionQuery.size;

const getHiddenName = (name: string) =>
accordionTitle === "Agency" ? `agency-${name}` : name;

const clearSection = useCallback(() => {
const currentSelections = new Set(
searchParams.get(camelCase(accordionTitle))?.split(","),
);
allSectionOptions.forEach((option) => {
currentSelections.delete(option);
});
toggleSelectAll(false, currentSelections);
}, [toggleSelectAll, accordionTitle, searchParams, allSectionOptions]);

return (
<div>
<button
Expand All @@ -69,10 +83,10 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
{childrenVisible ? (
<div className="padding-y-1">
<SearchFilterToggleAll
onSelectAll={() => toggleSelectAll(true, sectionAllSelected)}
onClearAll={() => toggleSelectAll(false, sectionAllSelected)}
onSelectAll={() => toggleSelectAll(true, allSectionOptions)}
onClearAll={() => clearSection()}
isAllSelected={isSectionAllSelected(
sectionAllSelected,
allSectionOptions,
sectionQuery,
)}
isNoneSelected={isSectionNoneSelected(sectionQuery)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ describe("SearchFilterAccordion", () => {
);

const accordionToggleButton = screen.getByTestId(
"accordionButton_funding-instrument-filter-status",
"accordionButton_opportunity-filter-status",
);
const contentDiv = screen.getByTestId(
"accordionItem_funding-instrument-filter-status",
"accordionItem_opportunity-filter-status",
);
expect(contentDiv).toHaveAttribute("hidden");

Expand Down
4 changes: 2 additions & 2 deletions frontend/tests/e2e/search/search-no-results.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, Page, test } from "@playwright/test";
import { BrowserContextOptions } from "playwright-core";

import {
expectURLContainsQueryParam,
expectURLContainsQueryParamValue,
fillSearchInputAndSubmit,
generateRandomString,
} from "./searchSpecUtil";
Expand All @@ -23,7 +23,7 @@ test.describe("Search page tests", () => {

await fillSearchInputAndSubmit(searchTerm, page);
await new Promise((resolve) => setTimeout(resolve, 3250));
expectURLContainsQueryParam(page, "query", searchTerm);
expectURLContainsQueryParamValue(page, "query", searchTerm);

// eslint-disable-next-line testing-library/prefer-screen-queries
const resultsHeading = page.getByRole("heading", {
Expand Down
Loading

0 comments on commit 257b7cf

Please sign in to comment.