diff --git a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx index 678831733e..bb698fb91b 100644 --- a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx +++ b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx @@ -23,6 +23,7 @@ import { toggleRefinement, sortBy, stats, + ratingMenu, } from '../widgets'; import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests'; @@ -268,6 +269,21 @@ const testSetups: TestSetupsMap = { }) .start(); }, + createRatingMenuWidgetTests({ instantSearchOptions, widgetParams }) { + instantsearch(instantSearchOptions) + .addWidgets([ + ratingMenu({ + container: document.body.appendChild(document.createElement('div')), + ...widgetParams, + }), + ]) + .on('error', () => { + /* + * prevent rethrowing InstantSearch errors, so tests can be asserted. + * IRL this isn't needed, as the error doesn't stop execution. */ + }) + .start(); + }, createInstantSearchWidgetTests({ instantSearchOptions }) { instantsearch(instantSearchOptions) .on('error', () => { @@ -457,6 +473,7 @@ const testOptions: TestOptionsMap = { createInfiniteHitsWidgetTests: undefined, createHitsWidgetTests: undefined, createRangeInputWidgetTests: undefined, + createRatingMenuWidgetTests: undefined, createInstantSearchWidgetTests: undefined, createHitsPerPageWidgetTests: undefined, createClearRefinementsWidgetTests: undefined, diff --git a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx index ef41a65fc5..61adf398d5 100644 --- a/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx +++ b/packages/instantsearch.js/src/components/RefinementList/RefinementList.tsx @@ -222,30 +222,25 @@ class RefinementList extends Component< return; } - if ( - !(originalEvent.target instanceof HTMLElement) || - !(originalEvent.target.parentNode instanceof HTMLElement) - ) { + let parent = originalEvent.target as HTMLElement | null; + + if (parent === null || parent.parentNode === null) { return; } if ( isRefined && - originalEvent.target.parentNode.querySelector( - 'input[type="radio"]:checked' - ) + parent.parentNode.querySelector('input[type="radio"]:checked') ) { // Prevent refinement for being reset if the user clicks on an already checked radio button return; } - if (originalEvent.target.tagName === 'INPUT') { + if (parent.tagName === 'INPUT') { this.refine(facetValueToRefine); return; } - let parent = originalEvent.target; - while (parent !== originalEvent.currentTarget) { if ( parent.tagName === 'LABEL' && diff --git a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx index 51de87e6d3..ba56712e58 100644 --- a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx +++ b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx @@ -269,6 +269,9 @@ const testSetups: TestSetupsMap = { ); }, + createRatingMenuWidgetTests() { + throw new Error('RatingMenu is not supported in React InstantSearch'); + }, createToggleRefinementWidgetTests({ instantSearchOptions, widgetParams }) { render( @@ -315,6 +318,12 @@ const testOptions: TestOptionsMap = { createInfiniteHitsWidgetTests: { act }, createHitsWidgetTests: { act }, createRangeInputWidgetTests: { act }, + createRatingMenuWidgetTests: { + act, + skippedTests: { + 'RatingMenu widget common tests': true, + }, + }, createInstantSearchWidgetTests: { act }, createHitsPerPageWidgetTests: { act }, createClearRefinementsWidgetTests: { act }, diff --git a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js index c927e81efa..c1a8025c20 100644 --- a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js @@ -24,6 +24,7 @@ import { AisToggleRefinement, AisSortBy, AisStats, + AisRatingMenu, } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; @@ -405,6 +406,21 @@ const testSetups = { await nextTick(); }, + async createRatingMenuWidgetTests({ instantSearchOptions, widgetParams }) { + mountApp( + { + render: renderCompat((h) => + h(AisInstantSearch, { props: instantSearchOptions }, [ + h(AisRatingMenu, { props: widgetParams }), + h(GlobalErrorSwallower), + ]) + ), + }, + document.body.appendChild(document.createElement('div')) + ); + + await nextTick(); + }, async createToggleRefinementWidgetTests({ instantSearchOptions, widgetParams, @@ -482,6 +498,7 @@ const testOptions = { createInfiniteHitsWidgetTests: undefined, createHitsWidgetTests: undefined, createRangeInputWidgetTests: undefined, + createRatingMenuWidgetTests: undefined, createInstantSearchWidgetTests: undefined, createHitsPerPageWidgetTests: undefined, createClearRefinementsWidgetTests: undefined, diff --git a/tests/common/widgets/index.ts b/tests/common/widgets/index.ts index f41b62911a..f04fa29500 100644 --- a/tests/common/widgets/index.ts +++ b/tests/common/widgets/index.ts @@ -8,6 +8,7 @@ export * from './instantsearch'; export * from './menu'; export * from './pagination'; export * from './range-input'; +export * from './rating-menu'; export * from './refinement-list'; export * from './hits-per-page'; export * from './toggle-refinement'; diff --git a/tests/common/widgets/rating-menu/behaviour.ts b/tests/common/widgets/rating-menu/behaviour.ts new file mode 100644 index 0000000000..9ca61af201 --- /dev/null +++ b/tests/common/widgets/rating-menu/behaviour.ts @@ -0,0 +1,114 @@ +import { + createSearchClient, + createMultiSearchResponse, + createSingleSearchResponse, +} from '@instantsearch/mocks'; +import { wait } from '@instantsearch/testutils'; +import userEvent from '@testing-library/user-event'; + +import type { RatingMenuWidgetSetup } from '.'; +import type { TestOptions } from '../../common'; + +export function createBehaviourTests( + setup: RatingMenuWidgetSetup, + { act }: Required +) { + describe('behaviour', () => { + test('handle refinement on click', async () => { + const delay = 100; + const margin = 10; + const attribute = 'brand'; + const options = { + instantSearchOptions: { + indexName: 'indexName', + searchClient: createSearchClient({ + search: jest.fn(async (requests) => { + await wait(delay); + return createMultiSearchResponse( + ...requests.map(() => + createSingleSearchResponse({ + facets: { + [attribute]: { + 0: 3422, + 1: 156, + 2: 194, + 3: 1622, + 4: 13925, + 5: 2150, + }, + }, + facets_stats: { + [attribute]: { + min: 1, + max: 5, + avg: 2, + sum: 71860, + }, + }, + }) + ) + ); + }), + }), + }, + widgetParams: { attribute }, + }; + + await setup(options); + + // Wait for initial results to populate widgets with data + await act(async () => { + await wait(margin + delay); + await wait(0); + }); + + // Initial state, before interaction + { + const items = document.querySelectorAll('.ais-RatingMenu-item'); + expect(items).toHaveLength(4); + + const selectedItems = document.querySelectorAll( + '.ais-RatingMenu-item--selected' + ); + expect(selectedItems).toHaveLength(0); + } + + // Refine on click of link + { + const firstItem = document.querySelector( + '.ais-RatingMenu-link' + )!; + + await act(async () => { + userEvent.click(firstItem); + await wait(0); + }); + + const selectedItems = document.querySelectorAll( + '.ais-RatingMenu-item--selected' + ); + expect(selectedItems).toHaveLength(1); + expect( + selectedItems[0].querySelector('.ais-RatingMenu-link') + ).toHaveAccessibleName(/4 & Up/i); + } + + // Refine on click of icon + { + const firstItem = document.querySelector( + '.ais-RatingMenu-starIcon' + )!; + + await act(async () => { + userEvent.click(firstItem); + await wait(0); + }); + + const selectedItems = document.querySelectorAll( + '.ais-RatingMenu-item--selected' + ); + expect(selectedItems).toHaveLength(0); + } + }); + }); +} diff --git a/tests/common/widgets/rating-menu/index.ts b/tests/common/widgets/rating-menu/index.ts new file mode 100644 index 0000000000..a50bfed31f --- /dev/null +++ b/tests/common/widgets/rating-menu/index.ts @@ -0,0 +1,24 @@ +import { fakeAct, skippableDescribe } from '../../common'; + +import { createBehaviourTests } from './behaviour'; + +import type { TestOptions, TestSetup } from '../../common'; +import type { RatingMenuWidget } from 'instantsearch.js/es/widgets/rating-menu/rating-menu'; + +type WidgetParams = Parameters[0]; +export type RatingMenuWidgetSetup = TestSetup<{ + widgetParams: Omit; +}>; + +export function createRatingMenuWidgetTests( + setup: RatingMenuWidgetSetup, + { act = fakeAct, skippedTests = {} }: TestOptions = {} +) { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + skippableDescribe('RatingMenu widget common tests', skippedTests, () => { + createBehaviourTests(setup, { act, skippedTests }); + }); +}