From adef3feb541e0cdba86c0583308431e7e5ad621b Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Wed, 19 Jun 2024 18:19:09 +0200 Subject: [PATCH] test(dynamic-widgets): common test suite (#6240) * test(dynamic-widgets): common test suite * add hierarchicalMenu and some refactoring --- .../src/__tests__/common-widgets.test.tsx | 24 +++ .../src/__tests__/common-widgets.test.tsx | 21 ++ .../src/__tests__/common-widgets.test.js | 28 +++ tests/common/widgets/dynamic-widgets/index.ts | 24 +++ .../common/widgets/dynamic-widgets/options.ts | 184 ++++++++++++++++++ tests/common/widgets/index.ts | 1 + 6 files changed, 282 insertions(+) create mode 100644 tests/common/widgets/dynamic-widgets/index.ts create mode 100644 tests/common/widgets/dynamic-widgets/options.ts diff --git a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx index f83f097faa..983279477c 100644 --- a/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx +++ b/packages/instantsearch.js/src/__tests__/common-widgets.test.tsx @@ -31,6 +31,7 @@ import { lookingSimilar, poweredBy, menuSelect, + dynamicWidgets, } from '../widgets'; import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests'; @@ -594,6 +595,28 @@ const testSetups: TestSetupsMap = { ]) .start(); }, + createDynamicWidgetsWidgetTests({ instantSearchOptions, widgetParams }) { + instantsearch(instantSearchOptions) + .addWidgets([ + dynamicWidgets({ + container: document.body.appendChild(document.createElement('div')), + widgets: [ + (container) => refinementList({ attribute: 'brand', container }), + (container) => menu({ attribute: 'category', container }), + (container) => + hierarchicalMenu({ + attributes: [ + 'hierarchicalCategories.lvl0', + 'hierarchicalCategories.lvl1', + ], + container, + }), + ], + ...widgetParams, + }), + ]) + .start(); + }, }; const testOptions: TestOptionsMap = { @@ -625,6 +648,7 @@ const testOptions: TestOptionsMap = { createLookingSimilarWidgetTests: undefined, createPoweredByWidgetTests: undefined, createMenuSelectWidgetTests: undefined, + createDynamicWidgetsWidgetTests: undefined, }; describe('Common widget tests (InstantSearch.js)', () => { diff --git a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx index 0595ee8684..65a8b8797d 100644 --- a/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx +++ b/packages/react-instantsearch/src/__tests__/common-widgets.test.tsx @@ -30,6 +30,7 @@ import { TrendingItems, LookingSimilar, PoweredBy, + DynamicWidgets, } from '..'; import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests'; @@ -372,6 +373,25 @@ const testSetups: TestSetupsMap = { createMenuSelectWidgetTests() { throw new Error('MenuSelect is not supported in React InstantSearch'); }, + createDynamicWidgetsWidgetTests({ instantSearchOptions, widgetParams }) { + render( + +
+ + + + + +
+ +
+ ); + }, }; const testOptions: TestOptionsMap = { @@ -414,6 +434,7 @@ const testOptions: TestOptionsMap = { 'MenuSelect widget common tests': true, }, }, + createDynamicWidgetsWidgetTests: { act }, }; /** diff --git a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js index ba2cf90d7b..65eda916e4 100644 --- a/packages/vue-instantsearch/src/__tests__/common-widgets.test.js +++ b/packages/vue-instantsearch/src/__tests__/common-widgets.test.js @@ -28,6 +28,7 @@ import { AisNumericMenu, AisPoweredBy, AisMenuSelect, + AisDynamicWidgets, } from '../instantsearch'; import { renderCompat } from '../util/vue-compat'; @@ -554,6 +555,32 @@ const testSetups = { await nextTick(); }, + createDynamicWidgetsWidgetTests({ instantSearchOptions, widgetParams }) { + mountApp( + { + render: renderCompat((h) => + h(AisInstantSearch, { props: instantSearchOptions }, [ + h( + AisDynamicWidgets, + { props: widgetParams }, + h(AisRefinementList, { props: { attribute: 'brand' } }), + h(AisMenu, { props: { attribute: 'category' } }), + h(AisHierarchicalMenu, { + props: { + attributes: [ + 'hierarchicalCategories.lvl0', + 'hierarchicalCategories.lvl1', + ], + }, + }) + ), + h(GlobalErrorSwallower), + ]) + ), + }, + document.body.appendChild(document.createElement('div')) + ); + }, }; const testOptions = { @@ -591,6 +618,7 @@ const testOptions = { skippedTests: { 'LookingSimilar widget common tests': true }, }, createPoweredByWidgetTests: undefined, + createDynamicWidgetsWidgetTests: undefined, }; describe('Common widget tests (Vue InstantSearch)', () => { diff --git a/tests/common/widgets/dynamic-widgets/index.ts b/tests/common/widgets/dynamic-widgets/index.ts new file mode 100644 index 0000000000..3cffe3eb1a --- /dev/null +++ b/tests/common/widgets/dynamic-widgets/index.ts @@ -0,0 +1,24 @@ +import { fakeAct } from '../../common'; + +import { createOptionsTests } from './options'; + +import type { TestOptions, TestSetup } from '../../common'; +import type { DynamicWidgetsWidget } from 'instantsearch.js/es/widgets/dynamic-widgets/dynamic-widgets'; + +type WidgetParams = Parameters[0]; +export type DynamicWidgetsWidgetSetup = TestSetup<{ + widgetParams: Omit; +}>; + +export function createDynamicWidgetsWidgetTests( + setup: DynamicWidgetsWidgetSetup, + { act = fakeAct, skippedTests = {} }: TestOptions = {} +) { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + describe('DynamicWidgets widget common tests', () => { + createOptionsTests(setup, { act, skippedTests }); + }); +} diff --git a/tests/common/widgets/dynamic-widgets/options.ts b/tests/common/widgets/dynamic-widgets/options.ts new file mode 100644 index 0000000000..3aef66de07 --- /dev/null +++ b/tests/common/widgets/dynamic-widgets/options.ts @@ -0,0 +1,184 @@ +import { + createAlgoliaSearchClient, + createMultiSearchResponse, + createSingleSearchResponse, +} from '@instantsearch/mocks'; +import { wait } from '@instantsearch/testutils'; +import { within, screen } from '@testing-library/dom'; + +import type { DynamicWidgetsWidgetSetup } from '.'; +import type { TestOptions } from '../../common'; + +export function createOptionsTests( + setup: DynamicWidgetsWidgetSetup, + { act }: Required +) { + describe('options', () => { + it('renders with default options', async () => { + const searchClient = createMockedSearchClient(); + + await setup({ + instantSearchOptions: { + searchClient, + indexName: 'indexName', + initialUiState: { + indexName: {}, + }, + }, + widgetParams: {}, + }); + + await act(async () => { + await wait(0); + }); + + const dynamicWidgets = Array.from( + document.querySelector('.ais-DynamicWidgets')!.childNodes + // Vue 3 outputs comment nodes + ).filter((node) => node.nodeType === Node.ELEMENT_NODE); + + expect(dynamicWidgets).toHaveLength(3); + + expect( + within(dynamicWidgets[0] as HTMLElement).getByRole('link', { + name: /tv/i, + }) + ).toBeInTheDocument(); + + expect( + within(dynamicWidgets[1] as HTMLElement).getByRole('checkbox', { + name: /samsung/i, + }) + ).toBeInTheDocument(); + + expect( + within(dynamicWidgets[2] as HTMLElement).getByRole('link', { + name: /books/i, + }) + ).toBeInTheDocument(); + }); + + it('forwards the `maxValuesPerFacet` option', async () => { + const searchClient = createMockedSearchClient(); + + await setup({ + instantSearchOptions: { + searchClient, + indexName: 'indexName', + initialUiState: { + indexName: {}, + }, + }, + widgetParams: { maxValuesPerFacet: 25 }, + }); + + await act(async () => { + await wait(0); + }); + + expect(searchClient.search).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + params: expect.objectContaining({ + maxValuesPerFacet: 25, + }), + }), + ]) + ); + }); + + it('forwards the `facets` option', async () => { + const searchClient = createMockedSearchClient(); + + await setup({ + instantSearchOptions: { + searchClient, + indexName: 'indexName', + initialUiState: { + indexName: {}, + }, + }, + widgetParams: { facets: ['other'] }, + }); + + await act(async () => { + await wait(0); + }); + + expect(searchClient.search).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + params: expect.objectContaining({ + facets: expect.arrayContaining(['other']), + }), + }), + ]) + ); + }); + + it('transforms items by calling the `transformItem` option', async () => { + const searchClient = createMockedSearchClient(); + + await setup({ + instantSearchOptions: { + searchClient, + indexName: 'indexName', + initialUiState: { + indexName: {}, + }, + }, + widgetParams: { + transformItems: () => ['brand'], + }, + }); + + await act(async () => { + await wait(0); + }); + + expect( + screen.queryByRole('checkbox', { name: /samsung/i }) + ).toBeInTheDocument(); + + expect( + screen.queryByRole('link', { name: /tv/i }) + ).not.toBeInTheDocument(); + }); + }); +} + +function createMockedSearchClient() { + return createAlgoliaSearchClient({ + search: jest.fn((requests) => { + return Promise.resolve( + createMultiSearchResponse( + ...requests.map(() => { + return createSingleSearchResponse({ + facets: { + brand: { + Samsung: 633, + Metra: 591, + }, + category: { + TV: 633, + Radio: 591, + }, + 'hierarchicalCategories.lvl0': { + Electronics: 633, + Books: 591, + }, + }, + renderingContent: { + facetOrdering: { + facets: { + order: ['category', 'brand', 'hierarchicalCategories.lvl0'], + }, + }, + }, + }); + }) + ) + ); + }), + }); +} diff --git a/tests/common/widgets/index.ts b/tests/common/widgets/index.ts index 56f66840ed..0e39a1f480 100644 --- a/tests/common/widgets/index.ts +++ b/tests/common/widgets/index.ts @@ -1,6 +1,7 @@ export * from './breadcrumb'; export * from './clear-refinements'; export * from './current-refinements'; +export * from './dynamic-widgets'; export * from './hierarchical-menu'; export * from './hits'; export * from './infinite-hits';