From ba3ec1a8994ce796aee10e2d79b1ba2a1bf64248 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Thu, 11 Jan 2024 18:37:44 +0100 Subject: [PATCH 1/2] fix(RatingMenu): handle clicks in svg Handle the clicks in the RefinementList component (used by RatingMenu) when the clicked element isn't a `HTMLElement`. The fixed behaviour is fairly finicky to reproduce, you need to click exactly on the "star image", if you click between them or on the text, no issue existed before this PR either. The removed check was introduced in https://github.com/algolia/instantsearch/pull/4702, when translating the component to TypeScript. This implements a barebones CTS for RatingMenu, including being fully skipped for React InstantSearch, as the widget doesn't exist. --- .../src/__tests__/common-widgets.test.tsx | 16 +++ .../RefinementList/RefinementList.tsx | 15 +-- .../src/__tests__/common-widgets.test.tsx | 9 ++ .../src/__tests__/common-widgets.test.js | 17 +++ tests/common/widgets/index.ts | 1 + tests/common/widgets/rating-menu/behaviour.ts | 114 ++++++++++++++++++ tests/common/widgets/rating-menu/index.ts | 24 ++++ 7 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 tests/common/widgets/rating-menu/behaviour.ts create mode 100644 tests/common/widgets/rating-menu/index.ts diff --git a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx index 678831733e..0bff9b4f34 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', () => { 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 }); + }); +} From 0ba1fd5a2ec1e1b8ad526d42835ab708d505cdf0 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Thu, 11 Jan 2024 18:55:39 +0100 Subject: [PATCH 2/2] lint --- packages/instantsearch.js/src/__tests__/common-widgets.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx index 0bff9b4f34..bb698fb91b 100644 --- a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx +++ b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx @@ -473,6 +473,7 @@ const testOptions: TestOptionsMap = { createInfiniteHitsWidgetTests: undefined, createHitsWidgetTests: undefined, createRangeInputWidgetTests: undefined, + createRatingMenuWidgetTests: undefined, createInstantSearchWidgetTests: undefined, createHitsPerPageWidgetTests: undefined, createClearRefinementsWidgetTests: undefined,