Skip to content

Commit

Permalink
fix(RatingMenu/RefinementList): handle clicks in svg (#5993)
Browse files Browse the repository at this point in the history
* 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 #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.

* lint
  • Loading branch information
Haroenv committed Jan 16, 2024
1 parent e748401 commit a6698f5
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 10 deletions.
17 changes: 17 additions & 0 deletions packages/instantsearch.js/src/__tests__/common-widgets.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
toggleRefinement,
sortBy,
stats,
ratingMenu,
} from '../widgets';

import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests';
Expand Down Expand Up @@ -268,6 +269,21 @@ const testSetups: TestSetupsMap<TestSuites> = {
})
.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', () => {
Expand Down Expand Up @@ -457,6 +473,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
createInfiniteHitsWidgetTests: undefined,
createHitsWidgetTests: undefined,
createRangeInputWidgetTests: undefined,
createRatingMenuWidgetTests: undefined,
createInstantSearchWidgetTests: undefined,
createHitsPerPageWidgetTests: undefined,
createClearRefinementsWidgetTests: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,30 +222,25 @@ class RefinementList<TTemplates extends Templates> 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' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ const testSetups: TestSetupsMap<TestSuites> = {
</form>
);
},
createRatingMenuWidgetTests() {
throw new Error('RatingMenu is not supported in React InstantSearch');
},
createToggleRefinementWidgetTests({ instantSearchOptions, widgetParams }) {
render(
<InstantSearch {...instantSearchOptions}>
Expand Down Expand Up @@ -315,6 +318,12 @@ const testOptions: TestOptionsMap<TestSuites> = {
createInfiniteHitsWidgetTests: { act },
createHitsWidgetTests: { act },
createRangeInputWidgetTests: { act },
createRatingMenuWidgetTests: {
act,
skippedTests: {
'RatingMenu widget common tests': true,
},
},
createInstantSearchWidgetTests: { act },
createHitsPerPageWidgetTests: { act },
createClearRefinementsWidgetTests: { act },
Expand Down
17 changes: 17 additions & 0 deletions packages/vue-instantsearch/src/__tests__/common-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AisToggleRefinement,
AisSortBy,
AisStats,
AisRatingMenu,
} from '../instantsearch';
import { renderCompat } from '../util/vue-compat';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -482,6 +498,7 @@ const testOptions = {
createInfiniteHitsWidgetTests: undefined,
createHitsWidgetTests: undefined,
createRangeInputWidgetTests: undefined,
createRatingMenuWidgetTests: undefined,
createInstantSearchWidgetTests: undefined,
createHitsPerPageWidgetTests: undefined,
createClearRefinementsWidgetTests: undefined,
Expand Down
1 change: 1 addition & 0 deletions tests/common/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
114 changes: 114 additions & 0 deletions tests/common/widgets/rating-menu/behaviour.ts
Original file line number Diff line number Diff line change
@@ -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<TestOptions>
) {
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<HTMLLIElement>(
'.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<HTMLLIElement>(
'.ais-RatingMenu-starIcon'
)!;

await act(async () => {
userEvent.click(firstItem);
await wait(0);
});

const selectedItems = document.querySelectorAll(
'.ais-RatingMenu-item--selected'
);
expect(selectedItems).toHaveLength(0);
}
});
});
}
24 changes: 24 additions & 0 deletions tests/common/widgets/rating-menu/index.ts
Original file line number Diff line number Diff line change
@@ -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<RatingMenuWidget>[0];
export type RatingMenuWidgetSetup = TestSetup<{
widgetParams: Omit<WidgetParams, 'container'>;
}>;

export function createRatingMenuWidgetTests(
setup: RatingMenuWidgetSetup,
{ act = fakeAct, skippedTests = {} }: TestOptions = {}
) {
beforeEach(() => {
document.body.innerHTML = '';
});

skippableDescribe('RatingMenu widget common tests', skippedTests, () => {
createBehaviourTests(setup, { act, skippedTests });
});
}

0 comments on commit a6698f5

Please sign in to comment.