diff --git a/packages/components/filters/src/badge/badge.spec.tsx b/packages/components/filters/src/badge/badge.spec.tsx index a67a52d5d5..d033dfaeb8 100644 --- a/packages/components/filters/src/badge/badge.spec.tsx +++ b/packages/components/filters/src/badge/badge.spec.tsx @@ -11,4 +11,18 @@ describe('Filters Badge', () => { const badge = await screen.findByRole('status'); expect(badge.textContent).toEqual('+1'); }); + + it('should render the badge with a custom aria-label attribute if provided', async () => { + const ariaLabel = 'custom aria-label'; + + render(); + const badge = await screen.findByRole('status', { name: ariaLabel }); + expect(badge).toBeInTheDocument(); + }); + + it('should apply disabled styles when isDisabled is true', () => { + render(); + const badge = screen.getByRole('status'); + expect(badge.className).toMatch(/disabledBadgeStyles/i); + }); }); diff --git a/packages/components/filters/src/badge/badge.tsx b/packages/components/filters/src/badge/badge.tsx index 032875edd4..ed6688ac40 100644 --- a/packages/components/filters/src/badge/badge.tsx +++ b/packages/components/filters/src/badge/badge.tsx @@ -37,7 +37,7 @@ const badgeStyles = css` height: ${designTokens.spacing40}; `; -const disabledBageStyles = css` +const disabledBadgeStyles = css` background-color: ${designTokens.colorNeutral}; `; @@ -45,7 +45,7 @@ function Badge(props: TBadgeProps) { return ( diff --git a/packages/components/filters/src/filter-menu/chip/chip.spec.tsx b/packages/components/filters/src/filter-menu/chip/chip.spec.tsx index 724c713c8e..54bbaed4a4 100644 --- a/packages/components/filters/src/filter-menu/chip/chip.spec.tsx +++ b/packages/components/filters/src/filter-menu/chip/chip.spec.tsx @@ -3,8 +3,14 @@ import Chip from './chip'; describe('FilterMenu Chip', () => { it('should render the chip', async () => { - await render(); + render(); const chip = await screen.findByRole('listitem'); expect(chip.textContent).toEqual('test'); }); + + it('should apply disabled styles when isDisabled is true', () => { + render(); + const chipElement = screen.getByRole('listitem'); + expect(chipElement.className).toMatch(/disabledChipStyles/i); + }); }); diff --git a/packages/components/filters/src/filters.spec.tsx b/packages/components/filters/src/filters.spec.tsx index fc48ea0275..07185fdaec 100644 --- a/packages/components/filters/src/filters.spec.tsx +++ b/packages/components/filters/src/filters.spec.tsx @@ -1,8 +1,18 @@ import { useState } from 'react'; -import { fireEvent, render, screen, within } from '../../../../test/test-utils'; import Filters, { TFiltersProps } from './filters'; -import { PrimaryColorsInput } from './fixtures/inputs'; import { FILTER_GROUP_KEYS } from './fixtures/constants'; +import { PrimaryColorsInput, ColorNameTextInput } from './fixtures/inputs'; +import { fireEvent, render, screen } from '../../../../test/test-utils'; +import { + getAddFilterButton, + getBadgeStatus, + getClearAllFiltersButton, + openAddFilterDialog, + displayedChips, + selectFilter, + selectFilterValues, + toggleFilterList, +} from './filters.spec.utils'; const mockRenderSearchComponent = ( @@ -28,7 +38,10 @@ const FilterTestComponent = ( } ) => { const [primaryColorValue, setPrimaryColorValue] = useState([]); + const [colorNameValue, setColorName] = useState(''); + const clearPrimaryColorFilter = () => setPrimaryColorValue([]); + const clearColorNameFilter = () => setColorName(''); const appliedFilters = []; @@ -41,6 +54,18 @@ const FilterTestComponent = ( })), }); } + + if (colorNameValue) { + appliedFilters.push({ + filterKey: 'colorName', + values: [ + { + value: colorNameValue, + label: colorNameValue, + }, + ], + }); + } const filters = [ { key: 'primaryColors', @@ -57,73 +82,207 @@ const FilterTestComponent = ( onClearRequest: clearPrimaryColorFilter, }, }, + { + key: 'colorName', + label: 'Color Name', + filterMenuConfiguration: { + renderMenuBody: () => ( + + ), + onClearRequest: clearColorNameFilter, + }, + }, ]; + return ( ); }; +//! should the add filter button be disabled once all possible filters are applied? +//! check on 1 applied filter badge - posed q in figma +//! user vs fireEvent? +//? assert chips are visible even before we escape filter dialog + describe('Filters', () => { it('should expand & collapse the filter list when `filters` button is clicked', async () => { render(); - const filtersButton = await screen.findByRole('button', { - name: /filters/i, - }); - // initial click will expand the filter list - fireEvent.click(filtersButton); - const addFilterButton = await screen.findByRole('button', { - name: /add filter/i, - }); + //Expand filter list & expect `Add filter` button to be visible + await toggleFilterList(); + const addFilterButton = await getAddFilterButton(); expect(addFilterButton).toBeVisible(); - // second click will collapse the filter list - fireEvent.click(filtersButton); + //Collapse filter list & expect `Add filter` button to be hidden + await toggleFilterList(); expect(addFilterButton).not.toBeVisible(); }); - it('should select a filter and a text input', async () => { + it('should apply values from multiple filters & display an applied filter count badge when collapsed', async () => { render(); - // expand filterList - const filtersButton = await screen.findByRole('button', { - name: /filters/i, - }); - fireEvent.click(filtersButton); + //Expand filter list + await toggleFilterList(); + const addFilterDialog = await openAddFilterDialog(); - // open add filter dialog - const addFilterButton = await screen.findByRole('button', { - name: /add filter/i, + //Filter 1 - Select Primary Colors filter & apply a single value + await selectFilter(addFilterDialog, 'Primary Colors'); + await selectFilterValues([/green/i]); + + //Expect chips to display selected filter values + const chips = displayedChips('primaryColors'); + expect(chips).toEqual(['green']); + + //Filter 2 - Select Color Name filter & enter a value + const addFilterDialog2 = await openAddFilterDialog(); + await selectFilter(addFilterDialog2, 'Color Name'); + const textbox = await screen.findByPlaceholderText(/enter a color name/i); + fireEvent.change(textbox, { + target: { + value: 'cobalt', + }, }); - fireEvent.click(addFilterButton); - // find dialog - const addFilterDialog = await screen.findByRole('dialog'); - // find option for filter to add - const option = within(addFilterDialog).getByText('Primary Colors'); - // select option - fireEvent.click(option); - // expect add filter dialog to close - expect(addFilterDialog).not.toBeInTheDocument(); - // expect dialog for selected filter to open - const selectFilterValuesDialog = await screen.findByRole('dialog'); - // get filter value to select - const filterValueOption = within(selectFilterValuesDialog).getByText( - /blue/i - ); - // select value - fireEvent.click(filterValueOption); - // close filter dialog - fireEvent.keyDown(filterValueOption, { - key: 'Escape', + + //Expect badge to not display if filter list is expanded + let filterTotalBadge = getBadgeStatus(); + expect(filterTotalBadge).toBeNull(); + + //Collapse filter list & expect filterDialogs to be hidden + await toggleFilterList(); + expect(addFilterDialog).not.toBeVisible(); + expect(addFilterDialog2).not.toBeVisible(); + + //Recapture the badge el + filterTotalBadge = getBadgeStatus(); + + //Expect badge to display count of applied filters visible + expect(filterTotalBadge).toHaveTextContent('2'); + expect(filterTotalBadge).toBeVisible(); + }); + + it('should apply multiple values from a single filter, display as chips, but display no badge on collapse', async () => { + render(); + + //Expand filter list & add filter + await toggleFilterList(); + const addFilterDialog = await openAddFilterDialog(); + + //Select Primary Colors filter & apply multiple values + await selectFilter(addFilterDialog, 'Primary Colors'); + await selectFilterValues([ + /blue/i, + /green/i, + /pink/i, + /lavender/i, + /azure/i, + ]); + + //Expect chips to display selected filter values + const chips = displayedChips('primaryColors'); + expect(chips).toEqual(['blue', 'green', 'pink', 'lavender', 'azure']); + + //Collapse filter list & expect no badge to display + await toggleFilterList(); + const filterTotalBadge = getBadgeStatus(); + expect(filterTotalBadge).toBeNull(); + }); + + //!is this needed? + it('should render search component', () => { + render(); + const searchInput = screen.getByPlaceholderText('Search Placeholder'); + expect(searchInput).toBeInTheDocument(); + }); + + it('should render `clear all` filters button & clear selections when clicked - >=2 applied filters', async () => { + render(); + + //Expand filter list + await toggleFilterList(); + const addFilterDialog = await openAddFilterDialog(); + + //Filter 1 - Select Primary Colors filter & apply a single value + await selectFilter(addFilterDialog, 'Primary Colors'); + await selectFilterValues([/green/i]); + + // Expect chips to display selected filter values + const chips = displayedChips('primaryColors'); + expect(chips).toEqual(['green']); + + //Filter 2 - Select Colors Name filter & enter a text value + const addFilterDialog2 = await openAddFilterDialog(); + await selectFilter(addFilterDialog2, 'Color Name'); + const textbox = await screen.findByPlaceholderText(/enter a color name/i); + fireEvent.change(textbox, { + target: { + value: 'Falu', + }, }); - expect(selectFilterValuesDialog).not.toBeInTheDocument(); - // check to make sure selected value is displayed in selected filter trigger button - const selectedValues = screen.getByRole('list', { + fireEvent.keyDown(textbox, { key: 'Escape' }); + + //Expect chips to display selected filter values + const selectedValues = screen.queryByRole('list', { name: 'primaryColors selected values', }); - const valueChip = within(selectedValues).getByRole('listitem'); + expect(selectedValues).toBeInTheDocument(); + + //Click `Clear all` (filters) button & expect applied filters to be cleared + const clearAllFiltersButton = getClearAllFiltersButton(); + if (clearAllFiltersButton) { + fireEvent.click(clearAllFiltersButton); + } + + expect(selectedValues).not.toBeInTheDocument(); + }); + + it('should not render `clear all` filters button with a single applied filter', async () => { + render(); + + //Expand filter list + await toggleFilterList(); + const addFilterDialog = await openAddFilterDialog(); + + //Filter 1 - select primary colors & apply a single value + await selectFilter(addFilterDialog, 'Primary Colors'); + await selectFilterValues([/green/i]); + + //Expect chips to display selected filter values + const chips = displayedChips('primaryColors'); + expect(chips).toEqual(['green']); + + //Expect Clear all` button to not be accessible + const clearAllFiltersButton = getClearAllFiltersButton(); + expect(clearAllFiltersButton).not.toBeInTheDocument(); + }); + + it('should remove applied filters individually', async () => { + render(); + + //Expand filter list + await toggleFilterList(); + const addFilterDialog = await openAddFilterDialog(); + + //Select Primary Colors filter & apply a single value + await selectFilter(addFilterDialog, 'Primary Colors'); + await selectFilterValues([/green/i]); + + //Expect chips to display selected filter values + const chips = displayedChips('primaryColors'); + expect(chips).toEqual(['green']); + + //Click the filter remove button & expect filter to be removed from list + const removeFilterButton = screen.queryByRole('button', { + name: /remove Primary Colors filter/i, + }); + + if (removeFilterButton) { + fireEvent.click(removeFilterButton); + } + + const primaryColorFilter = screen.queryByRole('button', { + name: /primary colors/i, + }); - expect(valueChip).toBeVisible(); - expect(valueChip).toHaveTextContent(/blue/i); + expect(primaryColorFilter).not.toBeInTheDocument(); }); }); diff --git a/packages/components/filters/src/filters.spec.utils.tsx b/packages/components/filters/src/filters.spec.utils.tsx new file mode 100644 index 0000000000..85a9084b82 --- /dev/null +++ b/packages/components/filters/src/filters.spec.utils.tsx @@ -0,0 +1,69 @@ +import { fireEvent, screen, within } from '../../../../test/test-utils'; + +/* Query & Utility Functions to be ued within filters.spec */ +const getFiltersButton = async (): Promise => { + return await screen.findByRole('button', { + name: /filters/i, + }); +}; + +export const getAddFilterButton = async (): Promise => { + return await screen.findByRole('button', { + name: /add filter/i, + }); +}; + +export const getClearAllFiltersButton = (): HTMLElement | null => { + return screen.queryByRole('button', { + name: /clear all/i, + }); +}; + +export const getBadgeStatus = (): HTMLElement | null => { + // Query matcher used here to cover positive & negative assertions + return screen.queryByRole('status'); +}; + +// Expand/ collapse filter list +export const toggleFilterList = async () => { + const filtersButton = await getFiltersButton(); + fireEvent.click(filtersButton); +}; + +//Click `Add filter` button & return dialog for interaction +export const openAddFilterDialog = async (): Promise => { + const addFilterButton = await getAddFilterButton(); + fireEvent.click(addFilterButton); + return await screen.findByRole('dialog'); +}; + +//Select filter to apply +export const selectFilter = async (dialog: HTMLElement, optionText: string) => { + const option = within(dialog).getByText(optionText); + fireEvent.click(option); +}; + +//Select filter values to apply +export const selectFilterValues = async ( + values: RegExp[] +): Promise => { + const selectFilterValuesDialog = await screen.findByRole('dialog'); + values.forEach(async (value) => { + const filterValueOption = within(selectFilterValuesDialog).getByText(value); + fireEvent.click(filterValueOption); + }); + fireEvent.keyDown(selectFilterValuesDialog, { key: 'Escape' }); + return !screen.queryByRole('dialog'); +}; + +// Retrieves selected values (chips) for a given filter +//! find better fn name +export const displayedChips = (filterName: string | null): string[] => { + const selectedValues = screen.getByRole('list', { + name: `${filterName} selected values`, + }); + const valueChips = within(selectedValues).getAllByRole('listitem'); + return valueChips + .map((chip) => chip.textContent) + .filter((text): text is string => text !== null); +}; diff --git a/packages/components/filters/src/filters.stories.tsx b/packages/components/filters/src/filters.stories.tsx index 881ad00483..498a2e94f7 100644 --- a/packages/components/filters/src/filters.stories.tsx +++ b/packages/components/filters/src/filters.stories.tsx @@ -148,7 +148,7 @@ type Story = StoryFn; export const BasicExample: Story = (props: TFiltersPropsWithCustomArgs) => { // simulate state from parent application for each menuBody input - const [primaryColorValue, setPrimaryColorValue] = useState(['red']); + const [primaryColorValue, setPrimaryColorValue] = useState([]); const [secondaryColorValue, setSecondaryColorValue] = useState([]); const [colorNameValue, setColorName] = useState(''); const [fruitsValue, setFruitsValue] = useState(''); diff --git a/packages/components/filters/src/filters.tsx b/packages/components/filters/src/filters.tsx index 1e54438fd6..8be48f0fbf 100644 --- a/packages/components/filters/src/filters.tsx +++ b/packages/components/filters/src/filters.tsx @@ -385,9 +385,8 @@ function Filters({ } defaultOpen={ activeFilterConfig.isPersistent || - (!showFilterControls && - localVisibleFilters.length === - visibleFiltersFromProps.length) + localVisibleFilters.length === + visibleFiltersFromProps.length ? false : true } diff --git a/packages/components/filters/src/fixtures/constants.tsx b/packages/components/filters/src/fixtures/constants.tsx index 7ad662c5d1..f3b0b23278 100644 --- a/packages/components/filters/src/fixtures/constants.tsx +++ b/packages/components/filters/src/fixtures/constants.tsx @@ -23,6 +23,11 @@ export const PRIMARY_COLOR_OPTIONS = [ }, { label: 'Indigo', value: 'indigo' }, { label: 'Violet', value: 'violet' }, + { label: 'Pink', value: 'pink' }, + { label: 'Azure', value: 'azure' }, + { label: 'Cobalt', value: 'cobalt' }, + { label: 'Ivory', value: 'ivory' }, + { label: 'Lavender', value: 'lavender' }, ]; export const SECONDARY_COLOR_OPTIONS = [