Skip to content

Commit

Permalink
feat(recommend): introduce Looking Similar React Hook and widget (#6184)
Browse files Browse the repository at this point in the history
* feat(recommend): add LookingSimilar and useLookingSimilar

FX-2768

* test(cts): enable widget test
  • Loading branch information
Haroenv authored and dhayab committed May 21, 2024
1 parent 6fae57c commit bc395c5
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import connectLookingSimilar from 'instantsearch.js/es/connectors/looking-similar/connectLookingSimilar';

import { useConnector } from '../hooks/useConnector';

import type { AdditionalWidgetProperties } from '../hooks/useConnector';
import type { BaseHit } from 'instantsearch.js';
import type {
LookingSimilarConnector,
LookingSimilarConnectorParams,
LookingSimilarWidgetDescription,
} from 'instantsearch.js/es/connectors/looking-similar/connectLookingSimilar';

export type UseLookingSimilarProps<THit extends BaseHit = BaseHit> =
LookingSimilarConnectorParams<THit>;

export function useLookingSimilar<THit extends BaseHit = BaseHit>(
props?: UseLookingSimilarProps<THit>,
additionalWidgetProperties?: AdditionalWidgetProperties
) {
return useConnector<
LookingSimilarConnectorParams<THit>,
LookingSimilarWidgetDescription<THit>
>(
connectLookingSimilar as LookingSimilarConnector<THit>,
props,
additionalWidgetProperties
);
}
1 change: 1 addition & 0 deletions packages/react-instantsearch-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export * from './connectors/useSortBy';
export * from './connectors/useStats';
export * from './connectors/useToggleRefinement';
export * from './connectors/useTrendingItems';
export * from './connectors/useLookingSimilar';
export * from './hooks/useConnector';
export * from './hooks/useInstantSearch';
export * from './lib/wrapPromiseWithState';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
useRelatedProducts,
useFrequentlyBoughtTogether,
useTrendingItems,
useLookingSimilar,
} from '..';

import type {
Expand All @@ -38,6 +39,7 @@ import type {
UseRelatedProductsProps,
UseFrequentlyBoughtTogetherProps,
UseTrendingItemsProps,
UseLookingSimilarProps,
} from '..';
import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests';
import type {
Expand Down Expand Up @@ -387,8 +389,27 @@ const testSetups: TestSetupsMap<TestSuites> = {
</InstantSearch>
);
},
createLookingSimilarConnectorTests: () => {
throw new Error('Not implemented');
createLookingSimilarConnectorTests: ({
instantSearchOptions,
widgetParams,
}) => {
function CustomLookingSimilar(props: UseLookingSimilarProps) {
const { recommendations } = useLookingSimilar(props);

return (
<ul>
{recommendations.map((recommendation) => (
<li key={recommendation.objectID}>{recommendation.objectID}</li>
))}
</ul>
);
}

render(
<InstantSearch {...instantSearchOptions}>
<CustomLookingSimilar {...widgetParams} />
</InstantSearch>
);
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
RelatedProducts,
FrequentlyBoughtTogether,
TrendingItems,
LookingSimilar,
} from '..';

import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests';
Expand Down Expand Up @@ -341,8 +342,13 @@ const testSetups: TestSetupsMap<TestSuites> = {
</InstantSearch>
);
},
createLookingSimilarTests() {
throw new Error('LookingSimilar is not supported in React InstantSearch');
createLookingSimilarTests({ instantSearchOptions, widgetParams }) {
render(
<InstantSearch {...instantSearchOptions}>
<LookingSimilar {...widgetParams} />
<GlobalErrorSwallower />
</InstantSearch>
);
},
};

Expand Down Expand Up @@ -378,12 +384,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
createRelatedProductsWidgetTests: { act },
createFrequentlyBoughtTogetherTests: { act },
createTrendingItemsWidgetTests: { act },
createLookingSimilarTests: {
act,
skippedTests: {
'LookingSimilar widget common tests': true,
},
},
createLookingSimilarTests: { act },
};

/**
Expand Down
72 changes: 72 additions & 0 deletions packages/react-instantsearch/src/widgets/LookingSimilar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { createLookingSimilarComponent } from 'instantsearch-ui-components';
import React, { createElement, Fragment } from 'react';
import { useLookingSimilar, useInstantSearch } from 'react-instantsearch-core';

import type {
LookingSimilarProps as LookingSimilarPropsUiComponentProps,
Pragma,
} from 'instantsearch-ui-components';
import type { Hit, BaseHit } from 'instantsearch.js';
import type { UseLookingSimilarProps } from 'react-instantsearch-core';

type UiProps<THit extends BaseHit> = Pick<
LookingSimilarPropsUiComponentProps<Hit<THit>>,
| 'items'
| 'itemComponent'
| 'headerComponent'
| 'fallbackComponent'
| 'status'
| 'sendEvent'
>;

export type LookingSimilarProps<THit extends BaseHit> = Omit<
LookingSimilarPropsUiComponentProps<Hit<THit>>,
keyof UiProps<THit>
> &
UseLookingSimilarProps<THit> & {
itemComponent?: LookingSimilarPropsUiComponentProps<THit>['itemComponent'];
headerComponent?: LookingSimilarPropsUiComponentProps<THit>['headerComponent'];
fallbackComponent?: LookingSimilarPropsUiComponentProps<THit>['fallbackComponent'];
};

const LookingSimilarUiComponent = createLookingSimilarComponent({
createElement: createElement as Pragma,
Fragment,
});

export function LookingSimilar<THit extends BaseHit = BaseHit>({
objectIDs,
maxRecommendations,
threshold,
queryParameters,
fallbackParameters,
transformItems,
itemComponent,
headerComponent,
fallbackComponent,
...props
}: LookingSimilarProps<THit>) {
const { status } = useInstantSearch();
const { recommendations } = useLookingSimilar<THit>(
{
objectIDs,
maxRecommendations,
threshold,
queryParameters,
fallbackParameters,
transformItems,
},
{ $$widgetType: 'ais.lookingSimilar' }
);

const uiProps: UiProps<THit> = {
items: recommendations,
itemComponent,
headerComponent,
fallbackComponent,
status,
sendEvent: () => {},
};

return <LookingSimilarUiComponent {...props} {...uiProps} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @jest-environment jsdom
*/

import {
createMultiSearchResponse,
createSearchClient,
createSingleSearchResponse,
} from '@instantsearch/mocks';
import { InstantSearchTestWrapper } from '@instantsearch/testutils';
import { render, waitFor } from '@testing-library/react';
import React from 'react';

import { LookingSimilar } from '../LookingSimilar';

import type { SearchClient } from 'instantsearch.js';

describe('LookingSimilar', () => {
test('renders with translations', async () => {
const client = createMockedSearchClient();
const { container } = render(
<InstantSearchTestWrapper searchClient={client}>
<LookingSimilar objectIDs={['1']} translations={{ title: 'My FBT' }} />
</InstantSearchTestWrapper>
);

await waitFor(() => {
expect(client.search).toHaveBeenCalledTimes(1);
});

await waitFor(() => {
expect(container.querySelector('.ais-LookingSimilar'))
.toMatchInlineSnapshot(`
<section
class="ais-LookingSimilar"
>
<h3
class="ais-LookingSimilar-title"
>
My FBT
</h3>
<div
class="ais-LookingSimilar-container"
>
<ol
class="ais-LookingSimilar-list"
>
<li
class="ais-LookingSimilar-item"
>
{
"objectID": "1"
}
</li>
<li
class="ais-LookingSimilar-item"
>
{
"objectID": "2"
}
</li>
</ol>
</div>
</section>
`);
});
});

test('forwards custom class names and `div` props to the root element', () => {
const { container } = render(
<InstantSearchTestWrapper>
<LookingSimilar
objectIDs={['1']}
className="MyLookingSimilar"
classNames={{ root: 'ROOT' }}
aria-hidden={true}
/>
</InstantSearchTestWrapper>
);

const root = container.firstChild;
expect(root).toHaveClass('MyLookingSimilar', 'ROOT');
expect(root).toHaveAttribute('aria-hidden', 'true');
});
});

function createMockedSearchClient() {
return createSearchClient({
getRecommendations: jest.fn((requests) =>
Promise.resolve(
createMultiSearchResponse(
// @ts-ignore
// `request` will be implicitly typed as `any` in type-check:v3
// since `getRecommendations` is not available there
...requests.map((request) => {
return createSingleSearchResponse<any>({
hits:
request.maxRecommendations === 0
? []
: [{ objectID: '1' }, { objectID: '2' }],
});
})
)
)
) as SearchClient['getRecommendations'],
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ function Widget<TWidget extends SingleWidget>({
return <widget.Component onSubmit={undefined} {...props} />;
}
case 'FrequentlyBoughtTogether':
case 'RelatedProducts': {
case 'RelatedProducts':
case 'LookingSimilar': {
return <widget.Component objectIDs={['1']} {...props} />;
}
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ describe('widgets', () => {
"$$widgetType": "ais.trendingItems",
"name": "TrendingItems",
},
{
"$$type": "ais.lookingSimilar",
"$$widgetType": "ais.lookingSimilar",
"name": "LookingSimilar",
},
]
`);
});
Expand Down
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './SortBy';
export * from './Stats';
export * from './ToggleRefinement';
export * from './TrendingItems';
export * from './LookingSimilar';

0 comments on commit bc395c5

Please sign in to comment.