Skip to content

Commit

Permalink
feat(recommend): introduce TrendingItems
Browse files Browse the repository at this point in the history
  • Loading branch information
Haroenv committed May 3, 2024
1 parent 9b7838b commit 1e539fb
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 10 deletions.
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "61.25 kB"
"maxSize": "61.75 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
21 changes: 12 additions & 9 deletions packages/react-instantsearch/src/__tests__/common-widgets.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
Stats,
RelatedProducts,
FrequentlyBoughtTogether,
TrendingItems,
} from '..';

import type { TestOptionsMap, TestSetupsMap } from '@instantsearch/tests';
Expand Down Expand Up @@ -328,9 +329,16 @@ const testSetups: TestSetupsMap<TestSuites> = {
</InstantSearch>
);
},
createTrendingItemsWidgetTests() {
throw new Error(
'TrendingItems is not implemented in React InstantSearch yet'
createTrendingItemsWidgetTests({ instantSearchOptions, widgetParams }) {
const { facetName, facetValue, ...params } = widgetParams;
const facetParams =
facetName && facetValue ? { facetName, facetValue } : {};

render(
<InstantSearch {...instantSearchOptions}>
<TrendingItems {...facetParams} {...params} />
<GlobalErrorSwallower />
</InstantSearch>
);
},
};
Expand Down Expand Up @@ -366,12 +374,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
},
createRelatedProductsWidgetTests: { act },
createFrequentlyBoughtTogetherTests: { act },
createTrendingItemsWidgetTests: {
act,
skippedTests: {
'TrendingItems widget common tests': true,
},
},
createTrendingItemsWidgetTests: { act },
};

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

import type {
TrendingItemsProps as TrendingItemsUiComponentProps,
Pragma,
RecordWithObjectID,
} from 'instantsearch-ui-components';
import type { Hit } from 'instantsearch.js';
import type { UseTrendingItemsProps } from 'react-instantsearch-core';

type UiProps<TItem extends RecordWithObjectID> = Pick<
TrendingItemsUiComponentProps<TItem>,
| 'items'
| 'itemComponent'
| 'headerComponent'
| 'fallbackComponent'
| 'status'
| 'sendEvent'
>;

export type TrendingItemsProps<TItem extends RecordWithObjectID> = Omit<
TrendingItemsUiComponentProps<TItem>,
keyof UiProps<TItem>
> &
UseTrendingItemsProps & {
itemComponent?: TrendingItemsUiComponentProps<TItem>['itemComponent'];
headerComponent?: TrendingItemsUiComponentProps<TItem>['headerComponent'];
fallbackComponent?: TrendingItemsUiComponentProps<TItem>['fallbackComponent'];
};

const TrendingItemsUiComponent = createTrendingItemsComponent({
createElement: createElement as Pragma,
Fragment,
});

export function TrendingItems<
TItem extends RecordWithObjectID = RecordWithObjectID
>({
facetName,
facetValue,
maxRecommendations,
threshold,
fallbackParameters,
queryParameters,
transformItems,
itemComponent,
headerComponent,
fallbackComponent,
...props
}: TrendingItemsProps<TItem>) {
const facetParameters =
facetName && facetValue ? { facetName, facetValue } : {};

const { status } = useInstantSearch();
const { recommendations } = useTrendingItems(
{
...facetParameters,
maxRecommendations,
threshold,
fallbackParameters,
queryParameters,
transformItems,
},
{ $$widgetType: 'ais.TrendingItems' }
);

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

return <TrendingItemsUiComponent {...props} {...uiProps} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @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 { TrendingItems } from '../TrendingItems';

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

describe('TrendingItems', () => {
test('renders with translations', async () => {
const client = createMockedSearchClient();
const { container } = render(
<InstantSearchTestWrapper searchClient={client}>
<TrendingItems translations={{ title: 'My trending items' }} />
</InstantSearchTestWrapper>
);

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

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

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

const root = container.firstChild;
expect(root).toHaveClass('MyTrendingItems', '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 @@ -145,6 +145,11 @@ describe('widgets', () => {
"$$widgetType": "ais.toggleRefinement",
"name": "ToggleRefinement",
},
{
"$$type": "ais.trendingItems",
"$$widgetType": "ais.TrendingItems",
"name": "TrendingItems",
},
]
`);
});
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 @@ -18,3 +18,4 @@ export * from './Snippet';
export * from './SortBy';
export * from './Stats';
export * from './ToggleRefinement';
export * from './TrendingItems';

0 comments on commit 1e539fb

Please sign in to comment.