diff --git a/dev/app/builtin/stories/breadcrumb.stories.js b/dev/app/builtin/stories/breadcrumb.stories.js
index 9961c0ede8..ff62023538 100644
--- a/dev/app/builtin/stories/breadcrumb.stories.js
+++ b/dev/app/builtin/stories/breadcrumb.stories.js
@@ -135,5 +135,40 @@ export default () => {
})
);
})
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ container.innerHTML = `
+
+
+ `;
+
+ window.search.addWidget(
+ instantsearch.widgets.breadcrumb({
+ container: '#breadcrumb',
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: `${item.name} (transformed)`,
+ })),
+ })
+ );
+
+ // Custom Widget to toggle refinement
+ window.search.addWidget({
+ init({ helper }) {
+ helper.toggleRefinement(
+ 'hierarchicalCategories.lvl0',
+ 'Cameras & Camcorders > Digital Cameras'
+ );
+ },
+ });
+ })
);
};
diff --git a/dev/app/builtin/stories/current-refined-values.stories.js b/dev/app/builtin/stories/current-refined-values.stories.js
index 7a4b00c845..96290f6942 100644
--- a/dev/app/builtin/stories/current-refined-values.stories.js
+++ b/dev/app/builtin/stories/current-refined-values.stories.js
@@ -80,5 +80,30 @@ export default () => {
},
}
)
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(
+ container => {
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinedValues({
+ container,
+ clearsQuery: true,
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: `${item.name} (transformed)`,
+ })),
+ })
+ );
+ },
+ {
+ searchParameters: {
+ disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
+ disjunctiveFacets: ['brand'],
+ numericRefinements: { price: { '>=': [100] } },
+ },
+ }
+ )
);
};
diff --git a/dev/app/builtin/stories/geo-search.stories.js b/dev/app/builtin/stories/geo-search.stories.js
index 65c473d1dc..5077f0c9d5 100644
--- a/dev/app/builtin/stories/geo-search.stories.js
+++ b/dev/app/builtin/stories/geo-search.stories.js
@@ -61,6 +61,30 @@ export default () => {
})
);
+ start();
+ })
+ )
+ ).add(
+ 'with transformed items',
+ wrapWithHitsAndConfiguration((container, start) =>
+ injectGoogleMaps(() => {
+ container.style.height = '600px';
+
+ window.search.addWidget(
+ instantsearch.widgets.geoSearch({
+ googleReference: window.google,
+ container,
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ _geoloc: {
+ lat: item._geoloc.lat + 2,
+ lng: item._geoloc.lng + 2,
+ },
+ })),
+ })
+ );
+
start();
})
)
diff --git a/dev/app/builtin/stories/hierarchical-menu.stories.js b/dev/app/builtin/stories/hierarchical-menu.stories.js
index 7916dc1b19..c9de784c8e 100644
--- a/dev/app/builtin/stories/hierarchical-menu.stories.js
+++ b/dev/app/builtin/stories/hierarchical-menu.stories.js
@@ -84,5 +84,25 @@ export default () => {
})
);
})
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hierarchicalMenu({
+ container,
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
);
};
diff --git a/dev/app/builtin/stories/hits-per-page-selector.stories.js b/dev/app/builtin/stories/hits-per-page-selector.stories.js
index 79d9919599..9ab9e53617 100644
--- a/dev/app/builtin/stories/hits-per-page-selector.stories.js
+++ b/dev/app/builtin/stories/hits-per-page-selector.stories.js
@@ -37,5 +37,25 @@ export default () => {
})
);
})
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hitsPerPageSelector({
+ container,
+ items: [
+ { value: 3, label: '3 per page' },
+ { value: 5, label: '5 per page' },
+ { value: 10, label: '10 per page' },
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
);
};
diff --git a/dev/app/builtin/stories/hits.stories.js b/dev/app/builtin/stories/hits.stories.js
index 7e34f94a28..435c3090bc 100644
--- a/dev/app/builtin/stories/hits.stories.js
+++ b/dev/app/builtin/stories/hits.stories.js
@@ -14,6 +14,21 @@ export default () => {
window.search.addWidget(instantsearch.widgets.hits({ container }));
})
)
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: `${item.name} (transformed)`,
+ })),
+ })
+ );
+ })
+ )
.add(
'with highlighted array',
wrapWithHits(
diff --git a/dev/app/builtin/stories/infinite-hits.stories.js b/dev/app/builtin/stories/infinite-hits.stories.js
index 9d7c2bbea1..ca0c519b47 100644
--- a/dev/app/builtin/stories/infinite-hits.stories.js
+++ b/dev/app/builtin/stories/infinite-hits.stories.js
@@ -44,5 +44,24 @@ export default () => {
})
);
})
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.infiniteHits({
+ container,
+ showMoreLabel: 'Show more',
+ templates: {
+ item: '{{name}}',
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: `${item.name} (transformed)`,
+ })),
+ })
+ );
+ })
);
};
diff --git a/dev/app/builtin/stories/menu.stories.js b/dev/app/builtin/stories/menu.stories.js
index 940c8e9682..700917fbfa 100644
--- a/dev/app/builtin/stories/menu.stories.js
+++ b/dev/app/builtin/stories/menu.stories.js
@@ -19,6 +19,22 @@ export default () => {
);
})
)
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.menu({
+ container,
+ attributeName: 'categories',
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
+ )
.add(
'with show more and header',
wrapWithHits(container => {
diff --git a/dev/app/builtin/stories/numeric-refinement-list.stories.js b/dev/app/builtin/stories/numeric-refinement-list.stories.js
index d8dca85c5b..40bc747bf0 100644
--- a/dev/app/builtin/stories/numeric-refinement-list.stories.js
+++ b/dev/app/builtin/stories/numeric-refinement-list.stories.js
@@ -7,32 +7,66 @@ import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('NumericRefinementList');
export default () => {
- stories.add(
- 'default',
- wrapWithHits(container => {
- window.search.addWidget(
- instantsearch.widgets.numericRefinementList({
- container,
- attributeName: 'price',
- operator: 'or',
- options: [
- { name: 'All' },
- { end: 4, name: 'less than 4' },
- { start: 4, end: 4, name: '4' },
- { start: 5, end: 10, name: 'between 5 and 10' },
- { start: 10, name: 'more than 10' },
- ],
- cssClasses: {
- header: 'facet-title',
- link: 'facet-value',
- count: 'facet-count pull-right',
- active: 'facet-active',
- },
- templates: {
- header: 'Numeric refinement list (price)',
- },
- })
- );
- })
- );
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.numericRefinementList({
+ container,
+ attributeName: 'price',
+ operator: 'or',
+ options: [
+ { name: 'All' },
+ { end: 4, name: 'less than 4' },
+ { start: 4, end: 4, name: '4' },
+ { start: 5, end: 10, name: 'between 5 and 10' },
+ { start: 10, name: 'more than 10' },
+ ],
+ cssClasses: {
+ header: 'facet-title',
+ link: 'facet-value',
+ count: 'facet-count pull-right',
+ active: 'facet-active',
+ },
+ templates: {
+ header: 'Numeric refinement list (price)',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with transformed hits',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.numericRefinementList({
+ container,
+ attributeName: 'price',
+ operator: 'or',
+ options: [
+ { name: 'All' },
+ { end: 4, name: 'less than 4' },
+ { start: 4, end: 4, name: '4' },
+ { start: 5, end: 10, name: 'between 5 and 10' },
+ { start: 10, name: 'more than 10' },
+ ],
+ cssClasses: {
+ header: 'facet-title',
+ link: 'facet-value',
+ count: 'facet-count pull-right',
+ active: 'facet-active',
+ },
+ templates: {
+ header: 'Numeric refinement list (price)',
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
+ );
};
diff --git a/dev/app/builtin/stories/numeric-selector.stories.js b/dev/app/builtin/stories/numeric-selector.stories.js
index 0d16363620..ad9ad21f36 100644
--- a/dev/app/builtin/stories/numeric-selector.stories.js
+++ b/dev/app/builtin/stories/numeric-selector.stories.js
@@ -45,4 +45,29 @@ export default () => {
);
})
);
+ stories.add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.numericSelector({
+ container,
+ operator: '=',
+ attributeName: 'rating',
+ options: [
+ { label: 'No rating selected', value: undefined },
+ { label: 'Rating: 5', value: 5 },
+ { label: 'Rating: 4', value: 4 },
+ { label: 'Rating: 3', value: 3 },
+ { label: 'Rating: 2', value: 2 },
+ { label: 'Rating: 1', value: 1 },
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
+ );
};
diff --git a/dev/app/builtin/stories/refinement-list.stories.js b/dev/app/builtin/stories/refinement-list.stories.js
index b6f952cc8c..979a6acc6c 100644
--- a/dev/app/builtin/stories/refinement-list.stories.js
+++ b/dev/app/builtin/stories/refinement-list.stories.js
@@ -115,5 +115,55 @@ export default () => {
})
);
})
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container,
+ attributeName: 'brand',
+ operator: 'or',
+ limit: 10,
+ templates: {
+ header: 'Transformed brands',
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ highlighted: `${item.highlighted} (transformed)`,
+ })),
+ })
+ );
+ })
+ )
+ .add(
+ 'with searchable transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container,
+ attributeName: 'brand',
+ operator: 'or',
+ limit: 10,
+ templates: {
+ header: 'Transformed searchable brands',
+ },
+ searchForFacetValues: {
+ placeholder: 'Find other brands...',
+ templates: {
+ noResults: 'No results',
+ },
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ highlighted: `${item.highlighted} (transformed)`,
+ })),
+ })
+ );
+ })
);
};
diff --git a/dev/app/builtin/stories/sort-by-selector.stories.js b/dev/app/builtin/stories/sort-by-selector.stories.js
index 7a87a18131..cbdc3df1a3 100644
--- a/dev/app/builtin/stories/sort-by-selector.stories.js
+++ b/dev/app/builtin/stories/sort-by-selector.stories.js
@@ -7,19 +7,40 @@ import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('SortBySelector');
export default () => {
- stories.add(
- 'default',
- wrapWithHits(container => {
- window.search.addWidget(
- instantsearch.widgets.sortBySelector({
- container,
- indices: [
- { name: 'instant_search', label: 'Most relevant' },
- { name: 'instant_search_price_asc', label: 'Lowest price' },
- { name: 'instant_search_price_desc', label: 'Highest price' },
- ],
- })
- );
- })
- );
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.sortBySelector({
+ container,
+ indices: [
+ { name: 'instant_search', label: 'Most relevant' },
+ { name: 'instant_search_price_asc', label: 'Lowest price' },
+ { name: 'instant_search_price_desc', label: 'Highest price' },
+ ],
+ })
+ );
+ })
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.sortBySelector({
+ container,
+ indices: [
+ { name: 'instant_search', label: 'Most relevant' },
+ { name: 'instant_search_price_asc', label: 'Lowest price' },
+ { name: 'instant_search_price_desc', label: 'Highest price' },
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
+ );
};
diff --git a/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js b/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
index 9a03124468..05ed133b09 100644
--- a/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
+++ b/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
@@ -202,6 +202,64 @@ describe('connectBreadcrumb', () => {
]);
});
+ it('provides the correct facet values when transformed', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectBreadcrumb(rendering);
+ const widget = makeWidget({
+ attributes: ['category', 'sub_category'],
+ transformItems: items =>
+ items.map(item => ({ ...item, name: 'transformed' })),
+ });
+
+ const config = widget.getConfiguration({});
+ const helper = jsHelper({}, '', config);
+ helper.search = jest.fn();
+
+ helper.toggleRefinement('category', 'Decoration');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.items).toEqual([]);
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ Decoration: 880,
+ },
+ subCategory: {
+ 'Decoration > Candle holders & candles': 193,
+ 'Decoration > Frames & pictures': 173,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ Decoration: 880,
+ Outdoor: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.items).toEqual([
+ expect.objectContaining({ name: 'transformed' }),
+ ]);
+ });
+
it('returns the correct URL', () => {
const rendering = jest.fn();
const makeWidget = connectBreadcrumb(rendering);
diff --git a/src/connectors/breadcrumb/connectBreadcrumb.js b/src/connectors/breadcrumb/connectBreadcrumb.js
index 2be5a76c72..f1ce40420e 100644
--- a/src/connectors/breadcrumb/connectBreadcrumb.js
+++ b/src/connectors/breadcrumb/connectBreadcrumb.js
@@ -16,6 +16,7 @@ search.addWidget(
customBreadcrumb({
attributes,
[ rootPath = null ],
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectBreadcrumb.html
@@ -31,6 +32,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @typedef {Object} CustomBreadcrumbWidgetOptions
* @property {string[]} attributes Attributes to use to generate the hierarchy of the breadcrumb.
* @property {string} [rootPath = null] Prefix path to use if the first level is not the root level.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
*/
@@ -56,7 +58,12 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
export default function connectBreadcrumb(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { attributes, separator = ' > ', rootPath = null } = widgetParams;
+ const {
+ attributes,
+ separator = ' > ',
+ rootPath = null,
+ transformItems = items => items,
+ } = widgetParams;
const [hierarchicalFacetName] = attributes;
if (!attributes || !Array.isArray(attributes) || attributes.length === 0) {
@@ -148,7 +155,9 @@ export default function connectBreadcrumb(renderFn, unmountFn) {
const [{ name: facetName }] = state.hierarchicalFacets;
const facetsValues = results.getFacetValues(facetName);
- const items = shiftItemsValues(prepareItems(facetsValues));
+ const items = transformItems(
+ shiftItemsValues(prepareItems(facetsValues))
+ );
renderFn(
{
diff --git a/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js b/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js
index 2384896ebc..23e54febb6 100644
--- a/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js
+++ b/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js
@@ -54,6 +54,44 @@ describe('connectCurrentRefinedValues', () => {
});
});
+ it('Renders transformed items during init and render', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['myFacet'],
+ });
+ helper.search = () => {};
+ const rendering = jest.fn();
+ const makeWidget = connectCurrentRefinedValues(rendering);
+ const widget = makeWidget({
+ transformItems: items =>
+ items.map(item => ({ ...item, name: 'transformed' })),
+ });
+
+ helper.addFacetRefinement('myFacet', 'value');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.refinements).toEqual([
+ expect.objectContaining({ name: 'transformed' }),
+ ]);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[0][0];
+ expect(secondRenderingOptions.refinements).toEqual([
+ expect.objectContaining({ name: 'transformed' }),
+ ]);
+ });
+
it('Provide a function to clear the refinement', () => {
// For each refinements we get a function that we can call
// for removing a single refinement
diff --git a/src/connectors/current-refined-values/connectCurrentRefinedValues.js b/src/connectors/current-refined-values/connectCurrentRefinedValues.js
index a36be3a0e6..cfcb7787df 100644
--- a/src/connectors/current-refined-values/connectCurrentRefinedValues.js
+++ b/src/connectors/current-refined-values/connectCurrentRefinedValues.js
@@ -34,7 +34,8 @@ search.addWidget(
customCurrentRefinedValues({
[ attributes = [] ],
[ onlyListedAttributes = false ],
- [ clearsQuery = false ]
+ [ clearsQuery = false ],
+ [ transformItems ],
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectCurrentRefinedValues.html
@@ -75,6 +76,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* set with no special treatment for the label.
* @property {boolean} [onlyListedAttributes = false] Limit the displayed refinement to the list specified.
* @property {boolean} [clearsQuery = false] Clears also the active search query when using clearAll.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -157,6 +159,7 @@ export default function connectCurrentRefinedValues(renderFn, unmountFn) {
attributes = [],
onlyListedAttributes = false,
clearsQuery = false,
+ transformItems = items => items,
} = widgetParams;
const attributesOK =
@@ -216,12 +219,14 @@ export default function connectCurrentRefinedValues(renderFn, unmountFn) {
clearRefinements({ helper, whiteList: restrictedTo, clearsQuery })
);
- const refinements = getFilteredRefinements(
- {},
- helper.state,
- attributeNames,
- onlyListedAttributes,
- clearsQuery
+ const refinements = transformItems(
+ getFilteredRefinements(
+ {},
+ helper.state,
+ attributeNames,
+ onlyListedAttributes,
+ clearsQuery
+ )
);
const _createURL = refinement =>
@@ -245,12 +250,14 @@ export default function connectCurrentRefinedValues(renderFn, unmountFn) {
},
render({ results, helper, state, createURL, instantSearchInstance }) {
- const refinements = getFilteredRefinements(
- results,
- state,
- attributeNames,
- onlyListedAttributes,
- clearsQuery
+ const refinements = transformItems(
+ getFilteredRefinements(
+ results,
+ state,
+ attributeNames,
+ onlyListedAttributes,
+ clearsQuery
+ )
);
const _createURL = refinement =>
diff --git a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
index c9fc8518f6..3eafaa21be 100644
--- a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
+++ b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
@@ -175,6 +175,48 @@ describe('connectGeoSearch - rendering', () => {
);
});
+ it('expect to render with transformed hits', () => {
+ const render = jest.fn();
+ const unmount = jest.fn();
+
+ const customGeoSearch = connectGeoSearch(render, unmount);
+ const widget = customGeoSearch({
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ _geoloc: {
+ lat: 20,
+ lng: 20,
+ },
+ })),
+ });
+
+ const helper = createFakeHelper({});
+
+ widget.render({
+ results: new SearchResults(helper.getState(), [
+ {
+ hits: [
+ { objectID: 123, _geoloc: { lat: 10, lng: 12 } },
+ { objectID: 456 },
+ { objectID: 789, _geoloc: { lat: 10, lng: 12 } },
+ ],
+ },
+ ]),
+ helper,
+ });
+
+ expect(render).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ { objectID: 123, _geoloc: { lat: 20, lng: 20 } },
+ { objectID: 789, _geoloc: { lat: 20, lng: 20 } },
+ ],
+ }),
+ false
+ );
+ });
+
it('expect to render with position from the state', () => {
const render = jest.fn();
const unmount = jest.fn();
diff --git a/src/connectors/geo-search/connectGeoSearch.js b/src/connectors/geo-search/connectGeoSearch.js
index 4dc3c4344f..17c293056d 100644
--- a/src/connectors/geo-search/connectGeoSearch.js
+++ b/src/connectors/geo-search/connectGeoSearch.js
@@ -27,6 +27,7 @@ search.addWidget(
[ position ],
[ radius ],
[ precision ],
+ [ transformItems ],
})
);
@@ -55,6 +56,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius) for more informations.
* @property {number} [precision] Precision of geo search (in meters).
* See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision) for more informations.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -137,6 +139,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
position,
radius,
precision,
+ transformItems = items => items,
} = widgetParams;
const widgetState = {
@@ -258,9 +261,11 @@ const connectGeoSearch = (renderFn, unmountFn) => {
renderArgs
);
+ const items = transformItems(results.hits.filter(hit => hit._geoloc));
+
renderFn(
{
- items: results.hits.filter(hit => hit._geoloc),
+ items,
position: getPositionFromState(state),
refine: refine(helper),
clearMapRefinement: clearMapRefinement(helper),
diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
index 99895a54ab..2d7c40802b 100644
--- a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
+++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper from 'algoliasearch-helper';
const SearchResults = jsHelper.SearchResults;
const SearchParameters = jsHelper.SearchParameters;
@@ -65,7 +63,7 @@ describe('connectHierarchicalMenu', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHierarchicalMenu(rendering);
const widget = makeWidget({
attributes: ['category', 'sub_category'],
@@ -86,10 +84,10 @@ describe('connectHierarchicalMenu', () => {
});
// test if widget is not rendered yet at this point
- expect(rendering.callCount).toBe(0);
+ expect(rendering).toHaveBeenCalledTimes(0);
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -99,12 +97,14 @@ describe('connectHierarchicalMenu', () => {
});
// test that rendering has been called during init with isFirstRendering = true
- expect(rendering.callCount).toBe(1);
+ expect(rendering).toHaveBeenCalledTimes(1);
// test if isFirstRendering is true during init
- expect(rendering.lastCall.args[1]).toBe(true);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- attributes: ['category', 'sub_category'],
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: { attributes: ['category', 'sub_category'] },
+ }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -114,22 +114,24 @@ describe('connectHierarchicalMenu', () => {
});
// test that rendering has been called during init with isFirstRendering = false
- expect(rendering.callCount).toBe(2);
- expect(rendering.lastCall.args[1]).toBe(false);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- attributes: ['category', 'sub_category'],
- });
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: { attributes: ['category', 'sub_category'] },
+ }),
+ false
+ );
});
it('Provide a function to clear the refinements at each step', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHierarchicalMenu(rendering);
const widget = makeWidget({
attributes: ['category', 'sub_category'],
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
helper.toggleRefinement('category', 'value');
@@ -140,7 +142,7 @@ describe('connectHierarchicalMenu', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
const { refine } = firstRenderingOptions;
refine('value');
expect(helper.hasRefinements('category')).toBe(false);
@@ -154,7 +156,7 @@ describe('connectHierarchicalMenu', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
const { refine: renderToggleRefinement } = secondRenderingOptions;
renderToggleRefinement('value');
expect(helper.hasRefinements('category')).toBe(false);
@@ -163,14 +165,14 @@ describe('connectHierarchicalMenu', () => {
});
it('provides the correct facet values', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHierarchicalMenu(rendering);
const widget = makeWidget({
attributes: ['category', 'subCategory'],
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
helper.toggleRefinement('category', 'Decoration');
@@ -181,11 +183,15 @@ describe('connectHierarchicalMenu', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
// During the first rendering there are no facet values
// The function get an empty array so that it doesn't break
// over null-ish values.
- expect(firstRenderingOptions.items).toEqual([]);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [],
+ }),
+ expect.anything()
+ );
widget.render({
results: new SearchResults(helper.state, [
@@ -215,38 +221,107 @@ describe('connectHierarchicalMenu', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.items).toEqual([
- {
- label: 'Decoration',
- value: 'Decoration',
- count: 880,
- isRefined: true,
- data: [
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
{
- label: 'Candle holders & candles',
- value: 'Decoration > Candle holders & candles',
- count: 193,
- isRefined: false,
- data: null,
+ label: 'Decoration',
+ value: 'Decoration',
+ count: 880,
+ isRefined: true,
+ data: [
+ {
+ label: 'Candle holders & candles',
+ value: 'Decoration > Candle holders & candles',
+ count: 193,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'Frames & pictures',
+ value: 'Decoration > Frames & pictures',
+ count: 173,
+ isRefined: false,
+ data: null,
+ },
+ ],
},
{
- label: 'Frames & pictures',
- value: 'Decoration > Frames & pictures',
- count: 173,
+ label: 'Outdoor',
+ value: 'Outdoor',
+ count: 47,
isRefined: false,
data: null,
},
],
- },
- {
- label: 'Outdoor',
- value: 'Outdoor',
- count: 47,
- isRefined: false,
- data: null,
- },
- ]);
+ }),
+ expect.anything()
+ );
+ });
+
+ it('provides the correct transformed facet values', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
+ const widget = makeWidget({
+ attributes: ['category', 'subCategory'],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: 'transformed',
+ })),
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ helper.toggleRefinement('category', 'Decoration');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({ items: [] }),
+ expect.anything()
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ Decoration: 880,
+ },
+ subCategory: {
+ 'Decoration > Candle holders & candles': 193,
+ 'Decoration > Frames & pictures': 173,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ Decoration: 880,
+ Outdoor: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ expect.objectContaining({ label: 'transformed' }),
+ expect.objectContaining({ label: 'transformed' }),
+ ],
+ }),
+ expect.anything()
+ );
});
describe('routing', () => {
@@ -258,7 +333,7 @@ describe('connectHierarchicalMenu', () => {
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
diff --git a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
index e966ca0796..e2562b0338 100644
--- a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
+++ b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
@@ -21,6 +21,7 @@ search.addWidget(
[ showParentLevel = true ],
[ limit = 10 ],
[ sortBy = ['name:asc'] ],
+ [ transformItems ],
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectHierarchicalMenu.html
@@ -46,6 +47,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -82,6 +84,7 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
showParentLevel = true,
limit = 10,
sortBy = ['name:asc'],
+ transformItems = items => items,
} = widgetParams;
if (!attributes || !attributes.length) {
@@ -168,9 +171,12 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
},
render({ results, state, createURL, instantSearchInstance }) {
- const items = this._prepareFacetValues(
- results.getFacetValues(hierarchicalFacetName, { sortBy }).data || [],
- state
+ const items = transformItems(
+ this._prepareFacetValues(
+ results.getFacetValues(hierarchicalFacetName, { sortBy }).data ||
+ [],
+ state
+ )
);
// Bind createURL to this specific attribute
diff --git a/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js b/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
index 96772b61a9..b024b8f6b7 100644
--- a/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
+++ b/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
@@ -1,4 +1,3 @@
-import sinon from 'sinon';
import jsHelper from 'algoliasearch-helper';
const SearchResults = jsHelper.SearchResults;
const SearchParameters = jsHelper.SearchParameters;
@@ -20,7 +19,7 @@ describe('connectHitsPerPage', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -33,12 +32,12 @@ describe('connectHitsPerPage', () => {
expect(widget.getConfiguration()).toEqual({});
// test if widget is not rendered yet at this point
- expect(rendering.callCount).toBe(0);
+ expect(rendering).toHaveBeenCalledTimes(0);
const helper = jsHelper({}, '', {
hitsPerPage: 3,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -48,15 +47,19 @@ describe('connectHitsPerPage', () => {
});
// test that rendering has been called during init with isFirstRendering = true
- expect(rendering.callCount).toBe(1);
+ expect(rendering).toHaveBeenCalledTimes(1);
// test if isFirstRendering is true during init
- expect(rendering.lastCall.args[1]).toBe(true);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- items: [
- { value: 3, label: '3 items per page' },
- { value: 10, label: '10 items per page' },
- ],
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: {
+ items: [
+ { value: 3, label: '3 items per page' },
+ { value: 10, label: '10 items per page' },
+ ],
+ },
+ }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -66,18 +69,71 @@ describe('connectHitsPerPage', () => {
});
// test that rendering has been called during init with isFirstRendering = false
- expect(rendering.callCount).toBe(2);
- expect(rendering.lastCall.args[1]).toBe(false);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: {
+ items: [
+ { value: 3, label: '3 items per page' },
+ { value: 10, label: '10 items per page' },
+ ],
+ },
+ }),
+ false
+ );
+ });
+
+ it('Renders during init and render with transformed items', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHitsPerPage(rendering);
+ const widget = makeWidget({
items: [
{ value: 3, label: '3 items per page' },
{ value: 10, label: '10 items per page' },
],
+ transformItems: items =>
+ items.map(item => ({ ...item, label: 'transformed' })),
+ });
+
+ const helper = jsHelper({}, '', {
+ hitsPerPage: 3,
+ });
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
});
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ expect.objectContaining({ label: 'transformed' }),
+ expect.objectContaining({ label: 'transformed' }),
+ ],
+ }),
+ true
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ expect.objectContaining({ label: 'transformed' }),
+ expect.objectContaining({ label: 'transformed' }),
+ ],
+ }),
+ false
+ );
});
it('Configures the search with the default hitsPerPage provided', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -92,7 +148,7 @@ describe('connectHitsPerPage', () => {
});
it('Does not configures the search when there is no default value', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -105,7 +161,7 @@ describe('connectHitsPerPage', () => {
});
it('Provide a function to change the current hits per page, and provide the current value', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -118,7 +174,7 @@ describe('connectHitsPerPage', () => {
const helper = jsHelper({}, '', {
hitsPerPage: 11,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -127,7 +183,7 @@ describe('connectHitsPerPage', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
const { refine } = firstRenderingOptions;
expect(helper.getQueryParameter('hitsPerPage')).toBe(11);
refine(3);
@@ -140,17 +196,17 @@ describe('connectHitsPerPage', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
const { refine: renderSetValue } = secondRenderingOptions;
expect(helper.getQueryParameter('hitsPerPage')).toBe(3);
renderSetValue(10);
expect(helper.getQueryParameter('hitsPerPage')).toBe(10);
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
});
it('provides the current hitsPerPage value', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -163,7 +219,7 @@ describe('connectHitsPerPage', () => {
const helper = jsHelper({}, '', {
hitsPerPage: 7,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -172,7 +228,7 @@ describe('connectHitsPerPage', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
expect(firstRenderingOptions.items).toMatchSnapshot();
firstRenderingOptions.refine(3);
@@ -183,12 +239,12 @@ describe('connectHitsPerPage', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toMatchSnapshot();
});
it('adds an option for the unselecting values, when the current hitsPerPage is defined elsewhere', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -200,7 +256,7 @@ describe('connectHitsPerPage', () => {
const helper = jsHelper({}, '', {
hitsPerPage: 7,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -209,7 +265,7 @@ describe('connectHitsPerPage', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
expect(firstRenderingOptions.items).toHaveLength(3);
firstRenderingOptions.refine(firstRenderingOptions.items[0].value);
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
@@ -224,14 +280,14 @@ describe('connectHitsPerPage', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toHaveLength(3);
secondRenderingOptions.refine(secondRenderingOptions.items[0].value);
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
});
it('the option for unselecting values should work even if stringified', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -243,7 +299,7 @@ describe('connectHitsPerPage', () => {
const helper = jsHelper({}, '', {
hitsPerPage: 7,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -252,7 +308,7 @@ describe('connectHitsPerPage', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
expect(firstRenderingOptions.items).toHaveLength(3);
firstRenderingOptions.refine(`${firstRenderingOptions.items[0].value}`);
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
@@ -267,14 +323,14 @@ describe('connectHitsPerPage', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toHaveLength(3);
secondRenderingOptions.refine(`${secondRenderingOptions.items[0].value}`);
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
});
it('Should be able to unselect using an empty string', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
const widget = makeWidget({
items: [
@@ -286,7 +342,7 @@ describe('connectHitsPerPage', () => {
const helper = jsHelper({}, '', {
hitsPerPage: 7,
});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -295,7 +351,7 @@ describe('connectHitsPerPage', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
expect(firstRenderingOptions.items).toHaveLength(3);
firstRenderingOptions.refine('');
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
@@ -310,7 +366,7 @@ describe('connectHitsPerPage', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toHaveLength(3);
secondRenderingOptions.refine('');
expect(helper.getQueryParameter('hitsPerPage')).not.toBeDefined();
@@ -328,7 +384,7 @@ describe('connectHitsPerPage', () => {
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
diff --git a/src/connectors/hits-per-page/connectHitsPerPage.js b/src/connectors/hits-per-page/connectHitsPerPage.js
index d46b2f2912..7894d4930a 100644
--- a/src/connectors/hits-per-page/connectHitsPerPage.js
+++ b/src/connectors/hits-per-page/connectHitsPerPage.js
@@ -20,6 +20,7 @@ search.addWidget(
{value: 10, label: '10 results per page'},
{value: 42, label: '42 results per page'},
],
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectHitsPerPage.html
@@ -50,6 +51,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} HitsPerPageWidgetOptions
* @property {HitsPerPageWidgetOptionsItem[]} items Array of objects defining the different values and labels.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -111,7 +113,7 @@ export default function connectHitsPerPage(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { items: userItems } = widgetParams;
+ const { items: userItems, transformItems = items => items } = widgetParams;
let items = userItems;
if (!items) {
@@ -167,7 +169,7 @@ The first one will be picked, you should probably set only one default value`
renderFn(
{
- items: this._transformItems(state),
+ items: transformItems(this._normalizeItems(state)),
refine: this.setHitsPerPage,
hasNoResults: true,
widgetParams,
@@ -182,7 +184,7 @@ The first one will be picked, you should probably set only one default value`
renderFn(
{
- items: this._transformItems(state),
+ items: transformItems(this._normalizeItems(state)),
refine: this.setHitsPerPage,
hasNoResults,
widgetParams,
@@ -192,7 +194,7 @@ The first one will be picked, you should probably set only one default value`
);
},
- _transformItems({ hitsPerPage }) {
+ _normalizeItems({ hitsPerPage }) {
return items.map(item => ({
...item,
isRefined: Number(item.value) === Number(hitsPerPage),
diff --git a/src/connectors/hits/__tests__/connectHits-test.js b/src/connectors/hits/__tests__/connectHits-test.js
index a574696ae0..23f9f588cb 100644
--- a/src/connectors/hits/__tests__/connectHits-test.js
+++ b/src/connectors/hits/__tests__/connectHits-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper from 'algoliasearch-helper';
const SearchResults = jsHelper.SearchResults;
@@ -9,7 +7,7 @@ describe('connectHits', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHits(rendering);
const widget = makeWidget({ escapeHits: true });
@@ -19,10 +17,10 @@ describe('connectHits', () => {
});
// test if widget is not rendered yet at this point
- expect(rendering.callCount).toBe(0);
+ expect(rendering).toHaveBeenCalledTimes(0);
const helper = jsHelper({}, '', {});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -31,13 +29,12 @@ describe('connectHits', () => {
onHistoryChange: () => {},
});
+ expect(rendering).toHaveBeenCalledTimes(1);
// test that rendering has been called during init with isFirstRendering = true
- expect(rendering.callCount).toBe(1);
- // test if isFirstRendering is true during init
- expect(rendering.lastCall.args[1]).toBe(true);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- escapeHits: true,
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({ widgetParams: { escapeHits: true } }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -46,21 +43,21 @@ describe('connectHits', () => {
createURL: () => '#',
});
+ expect(rendering).toHaveBeenCalledTimes(2);
// test that rendering has been called during init with isFirstRendering = false
- expect(rendering.callCount).toBe(2);
- expect(rendering.lastCall.args[1]).toBe(false);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- escapeHits: true,
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({ widgetParams: { escapeHits: true } }),
+ false
+ );
});
it('Provides the hits and the whole results', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHits(rendering);
const widget = makeWidget({});
const helper = jsHelper({}, '', {});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -69,9 +66,13 @@ describe('connectHits', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
- expect(firstRenderingOptions.hits).toEqual([]);
- expect(firstRenderingOptions.results).toBe(undefined);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ hits: [],
+ results: undefined,
+ }),
+ expect.anything()
+ );
const hits = [{ fake: 'data' }, { sample: 'infos' }];
@@ -85,18 +86,22 @@ describe('connectHits', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.hits).toEqual(hits);
- expect(secondRenderingOptions.results).toEqual(results);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ hits,
+ results,
+ }),
+ expect.anything()
+ );
});
it('escape highlight properties if requested', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectHits(rendering);
const widget = makeWidget({ escapeHits: true });
const helper = jsHelper({}, '', {});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -105,9 +110,13 @@ describe('connectHits', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
- expect(firstRenderingOptions.hits).toEqual([]);
- expect(firstRenderingOptions.results).toBe(undefined);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ hits: [],
+ results: undefined,
+ }),
+ expect.anything()
+ );
const hits = [
{
@@ -139,8 +148,55 @@ describe('connectHits', () => {
escapedHits.__escaped = true;
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.hits).toEqual(escapedHits);
- expect(secondRenderingOptions.results).toEqual(results);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ hits: escapedHits,
+ results,
+ }),
+ expect.anything()
+ );
+ });
+
+ it('transform items if requested', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHits(rendering);
+ const widget = makeWidget({
+ transformItems: items => items.map(() => ({ name: 'transformed' })),
+ });
+
+ const helper = jsHelper({}, '', {});
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ expect(rendering).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({ hits: [], results: undefined }),
+ expect.anything()
+ );
+
+ const hits = [{ name: 'name 1' }, { name: 'name 2' }];
+
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ hits: [{ name: 'transformed' }, { name: 'transformed' }],
+ results,
+ }),
+ expect.anything()
+ );
});
});
diff --git a/src/connectors/hits/connectHits.js b/src/connectors/hits/connectHits.js
index 02d4fc9b31..f81f08b0ab 100644
--- a/src/connectors/hits/connectHits.js
+++ b/src/connectors/hits/connectHits.js
@@ -12,7 +12,8 @@ var customHits = connectHits(function render(params, isFirstRendering) {
});
search.addWidget(
customHits({
- [ escapeHits = false ]
+ [ escapeHits = false ],
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectHits.html
@@ -28,6 +29,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomHitsWidgetOptions
* @property {boolean} [escapeHits = false] If true, escape HTML tags from `hits[i]._highlightResult`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -59,41 +61,51 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
export default function connectHits(renderFn, unmountFn) {
checkRendering(renderFn, usage);
- return (widgetParams = {}) => ({
- getConfiguration() {
- return widgetParams.escapeHits ? tagConfig : undefined;
- },
+ return (widgetParams = {}) => {
+ const { transformItems = items => items } = widgetParams;
- init({ instantSearchInstance }) {
- renderFn(
- {
- hits: [],
- results: undefined,
- instantSearchInstance,
- widgetParams,
- },
- true
- );
- },
+ return {
+ getConfiguration() {
+ return widgetParams.escapeHits ? tagConfig : undefined;
+ },
- render({ results, instantSearchInstance }) {
- if (widgetParams.escapeHits && results.hits && results.hits.length > 0) {
- results.hits = escapeHits(results.hits);
- }
+ init({ instantSearchInstance }) {
+ renderFn(
+ {
+ hits: [],
+ results: undefined,
+ instantSearchInstance,
+ widgetParams,
+ },
+ true
+ );
+ },
- renderFn(
- {
- hits: results.hits,
- results,
- instantSearchInstance,
- widgetParams,
- },
- false
- );
- },
+ render({ results, instantSearchInstance }) {
+ results.hits = transformItems(results.hits);
- dispose() {
- unmountFn();
- },
- });
+ if (
+ widgetParams.escapeHits &&
+ results.hits &&
+ results.hits.length > 0
+ ) {
+ results.hits = escapeHits(results.hits);
+ }
+
+ renderFn(
+ {
+ hits: results.hits,
+ results,
+ instantSearchInstance,
+ widgetParams,
+ },
+ false
+ );
+ },
+
+ dispose() {
+ unmountFn();
+ },
+ };
+ };
}
diff --git a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
index e75df1cadb..3c88cd8512 100644
--- a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
+++ b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
@@ -202,6 +202,58 @@ describe('connectInfiniteHits', () => {
expect(secondRenderingOptions.results).toEqual(results);
});
+ it('transform items if requested', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectInfiniteHits(rendering);
+ const widget = makeWidget({
+ transformItems: items => items.map(() => ({ name: 'transformed' })),
+ });
+
+ const helper = jsHelper({}, '', {});
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.hits).toEqual([]);
+ expect(firstRenderingOptions.results).toBe(undefined);
+
+ const hits = [
+ {
+ name: 'name 1',
+ },
+ {
+ name: 'name 2',
+ },
+ ];
+
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const transformedHits = [
+ {
+ name: 'transformed',
+ },
+ {
+ name: 'transformed',
+ },
+ ];
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.hits).toEqual(transformedHits);
+ expect(secondRenderingOptions.results).toEqual(results);
+ });
+
it('does not render the same page twice', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
diff --git a/src/connectors/infinite-hits/connectInfiniteHits.js b/src/connectors/infinite-hits/connectInfiniteHits.js
index 47e15fed82..1bf7a08e3e 100644
--- a/src/connectors/infinite-hits/connectInfiniteHits.js
+++ b/src/connectors/infinite-hits/connectInfiniteHits.js
@@ -14,7 +14,8 @@ var customInfiniteHits = connectInfiniteHits(function render(params, isFirstRend
});
search.addWidget(
customInfiniteHits({
- escapeHits: true,
+ [ escapeHits: true ],
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectInfiniteHits.html
@@ -32,6 +33,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomInfiniteHitsWidgetOptions
* @property {boolean} [escapeHits = false] If true, escape HTML tags from `hits[i]._highlightResult`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -78,6 +80,7 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
+ const { transformItems = items => items } = widgetParams;
let hitsCache = [];
let lastReceivedPage = -1;
@@ -110,6 +113,8 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
lastReceivedPage = -1;
}
+ results.hits = transformItems(results.hits);
+
if (
widgetParams.escapeHits &&
results.hits &&
diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.js
index 2fa6451cf0..14a6d55224 100644
--- a/src/connectors/menu/__tests__/connectMenu-test.js
+++ b/src/connectors/menu/__tests__/connectMenu-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
@@ -11,7 +9,7 @@ describe('connectMenu', () => {
let rendering;
let makeWidget;
beforeEach(() => {
- rendering = sinon.stub();
+ rendering = jest.fn();
makeWidget = connectMenu(rendering);
});
@@ -82,10 +80,10 @@ describe('connectMenu', () => {
});
// test if widget is not rendered yet at this point
- expect(rendering.callCount).toBe(0);
+ expect(rendering).toHaveBeenCalledTimes(0);
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -95,16 +93,18 @@ describe('connectMenu', () => {
});
// test that rendering has been called during init with isFirstRendering = true
- expect(rendering.callCount).toBe(1);
+ expect(rendering).toHaveBeenCalledTimes(1);
// test if isFirstRendering is true during init
- expect(rendering.lastCall.args[1]).toBe(true);
-
- const firstRenderingOptions = rendering.lastCall.args[0];
- expect(firstRenderingOptions.canRefine).toBe(false);
- expect(firstRenderingOptions.widgetParams).toEqual({
- attributeName: 'myFacet',
- limit: 9,
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ canRefine: false,
+ widgetParams: {
+ attributeName: 'myFacet',
+ limit: 9,
+ },
+ }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -114,15 +114,17 @@ describe('connectMenu', () => {
});
// test that rendering has been called during init with isFirstRendering = false
- expect(rendering.callCount).toBe(2);
- expect(rendering.lastCall.args[1]).toBe(false);
-
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.canRefine).toBe(false);
- expect(secondRenderingOptions.widgetParams).toEqual({
- attributeName: 'myFacet',
- limit: 9,
- });
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ canRefine: false,
+ widgetParams: {
+ attributeName: 'myFacet',
+ limit: 9,
+ },
+ }),
+ false
+ );
});
it('Provide a function to clear the refinements at each step', () => {
@@ -131,7 +133,7 @@ describe('connectMenu', () => {
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
helper.toggleRefinement('category', 'value');
@@ -142,7 +144,7 @@ describe('connectMenu', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
const { refine } = firstRenderingOptions;
refine('value');
expect(helper.hasRefinements('category')).toBe(false);
@@ -156,7 +158,7 @@ describe('connectMenu', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
const { refine: renderRefine } = secondRenderingOptions;
renderRefine('value');
expect(helper.hasRefinements('category')).toBe(false);
@@ -170,7 +172,7 @@ describe('connectMenu', () => {
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
helper.toggleRefinement('category', 'Decoration');
@@ -181,11 +183,13 @@ describe('connectMenu', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
// During the first rendering there are no facet values
// The function get an empty array so that it doesn't break
// over null-ish values.
- expect(firstRenderingOptions.items).toEqual([]);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({ items: [] }),
+ expect.anything()
+ );
widget.render({
results: new SearchResults(helper.state, [
@@ -211,23 +215,88 @@ describe('connectMenu', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.items).toEqual([
- {
- label: 'Decoration',
- value: 'Decoration',
- count: 880,
- isRefined: true,
- data: null,
- },
- {
- label: 'Outdoor',
- value: 'Outdoor',
- count: 47,
- isRefined: false,
- data: null,
- },
- ]);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'Decoration',
+ value: 'Decoration',
+ count: 880,
+ isRefined: true,
+ data: null,
+ },
+ {
+ label: 'Outdoor',
+ value: 'Outdoor',
+ count: 47,
+ isRefined: false,
+ data: null,
+ },
+ ],
+ }),
+ expect.anything()
+ );
+ });
+
+ it('provides the correct transformed facet values', () => {
+ const widget = makeWidget({
+ attributeName: 'category',
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: 'transformed',
+ })),
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ helper.toggleRefinement('category', 'Decoration');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [],
+ }),
+ expect.anything()
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ Decoration: 880,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ Decoration: 880,
+ Outdoor: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ expect.objectContaining({ label: 'transformed' }),
+ expect.objectContaining({ label: 'transformed' }),
+ ],
+ }),
+ expect.anything()
+ );
});
describe('showMore', () => {
@@ -279,9 +348,12 @@ describe('connectMenu', () => {
onHistoryChange: () => {},
});
- // Then
- const firstRenderingOptions = rendering.lastCall.args[0];
- expect(firstRenderingOptions.isShowingMore).toBe(false);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ isShowingMore: false,
+ }),
+ expect.anything()
+ );
});
it('should toggle `isShowingMore` when `toggleShowMore` is called', () => {
@@ -331,7 +403,8 @@ describe('connectMenu', () => {
});
// Then
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(firstRenderingOptions.isShowingMore).toBe(false);
expect(firstRenderingOptions.items).toHaveLength(1);
expect(firstRenderingOptions.canToggleShowMore).toBe(true);
@@ -340,7 +413,8 @@ describe('connectMenu', () => {
firstRenderingOptions.toggleShowMore();
// Then
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(secondRenderingOptions.isShowingMore).toBe(true);
expect(secondRenderingOptions.items).toHaveLength(2);
expect(firstRenderingOptions.canToggleShowMore).toBe(true);
@@ -391,7 +465,8 @@ describe('connectMenu', () => {
createURL: () => '#',
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(firstRenderingOptions.items).toHaveLength(1);
expect(firstRenderingOptions.canToggleShowMore).toBe(false);
});
@@ -406,7 +481,7 @@ describe('connectMenu', () => {
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
diff --git a/src/connectors/menu/connectMenu.js b/src/connectors/menu/connectMenu.js
index 44e44b3127..d5deda0e39 100644
--- a/src/connectors/menu/connectMenu.js
+++ b/src/connectors/menu/connectMenu.js
@@ -19,6 +19,7 @@ search.addWidget(
[ limit ],
[ showMoreLimit ]
[ sortBy = ['name:asc'] ]
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectMenu.html
@@ -40,6 +41,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -113,6 +115,7 @@ export default function connectMenu(renderFn, unmountFn) {
limit = 10,
sortBy = ['name:asc'],
showMoreLimit,
+ transformItems = items => items,
} = widgetParams;
if (!attributeName || (!isNaN(showMoreLimit) && showMoreLimit < limit)) {
@@ -200,13 +203,15 @@ export default function connectMenu(renderFn, unmountFn) {
render({ results, instantSearchInstance }) {
const facetItems =
results.getFacetValues(attributeName, { sortBy }).data || [];
- const items = facetItems
- .slice(0, this.getLimit())
- .map(({ name: label, path: value, ...item }) => ({
- ...item,
- label,
- value,
- }));
+ const items = transformItems(
+ facetItems
+ .slice(0, this.getLimit())
+ .map(({ name: label, path: value, ...item }) => ({
+ ...item,
+ label,
+ value,
+ }))
+ );
this.toggleShowMore = this.createToggleShowMore({
results,
diff --git a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js b/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js
index 7cc456f3d3..69d0e10840 100644
--- a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js
+++ b/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js
@@ -1,4 +1,3 @@
-import sinon from 'sinon';
import jsHelper, {
SearchResults,
SearchParameters,
@@ -18,7 +17,7 @@ describe('connectNumericRefinementList', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectNumericRefinementList(rendering);
const widget = makeWidget({
attributeName: 'numerics',
@@ -32,10 +31,10 @@ describe('connectNumericRefinementList', () => {
expect(widget.getConfiguration).toBe(undefined);
// test if widget is not rendered yet at this point
- expect(rendering.callCount).toBe(0);
+ expect(rendering).toHaveBeenCalledTimes(0);
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -45,17 +44,21 @@ describe('connectNumericRefinementList', () => {
});
// test that rendering has been called during init with isFirstRendering = true
- expect(rendering.callCount).toBe(1);
+ expect(rendering).toHaveBeenCalledTimes(1);
// test if isFirstRendering is true during init
- expect(rendering.lastCall.args[1]).toBe(true);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- ],
- });
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: {
+ attributeName: 'numerics',
+ options: [
+ { name: 'below 10', end: 10 },
+ { name: '10 - 20', start: 10, end: 20 },
+ { name: 'more than 20', start: 20 },
+ ],
+ },
+ }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{ nbHits: 0 }]),
@@ -65,20 +68,69 @@ describe('connectNumericRefinementList', () => {
});
// test that rendering has been called during init with isFirstRendering = false
- expect(rendering.callCount).toBe(2);
- expect(rendering.lastCall.args[1]).toBe(false);
- expect(rendering.lastCall.args[0].widgetParams).toEqual({
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ widgetParams: {
+ attributeName: 'numerics',
+ options: [
+ { name: 'below 10', end: 10 },
+ { name: '10 - 20', start: 10, end: 20 },
+ { name: 'more than 20', start: 20 },
+ ],
+ },
+ }),
+ false
+ );
+ });
+
+ it('Renders during init and render with transformed items', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectNumericRefinementList(rendering);
+ const widget = makeWidget({
attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- ],
+ options: [{ name: 'below 10', end: 10 }],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: 'transformed',
+ })),
});
+
+ const helper = jsHelper({});
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [expect.objectContaining({ label: 'transformed' })],
+ }),
+ expect.anything()
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [{ nbHits: 0 }]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [expect.objectContaining({ label: 'transformed' })],
+ }),
+ expect.anything()
+ );
});
it('Provide a function to update the refinements at each step', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectNumericRefinementList(rendering);
const widget = makeWidget({
attributeName: 'numerics',
@@ -92,7 +144,7 @@ describe('connectNumericRefinementList', () => {
});
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -101,7 +153,7 @@ describe('connectNumericRefinementList', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions = rendering.mock.calls[0][0];
const { refine, items } = firstRenderingOptions;
expect(helper.state.getNumericRefinements('numerics')).toEqual({});
refine(items[0].value);
@@ -131,7 +183,7 @@ describe('connectNumericRefinementList', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions = rendering.mock.calls[1][0];
const {
refine: renderToggleRefinement,
items: renderFacetValues,
@@ -159,7 +211,7 @@ describe('connectNumericRefinementList', () => {
});
it('provides the correct facet values', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectNumericRefinementList(rendering);
const widget = makeWidget({
attributeName: 'numerics',
@@ -171,7 +223,7 @@ describe('connectNumericRefinementList', () => {
});
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -180,16 +232,20 @@ describe('connectNumericRefinementList', () => {
onHistoryChange: () => {},
});
- const firstRenderingOptions = rendering.lastCall.args[0];
- expect(firstRenderingOptions.items).toEqual([
- {
- label: 'below 10',
- value: encodeValue(undefined, 10),
- isRefined: false,
- },
- { label: '10 - 20', value: encodeValue(10, 20), isRefined: false },
- { label: 'more than 20', value: encodeValue(20), isRefined: false },
- ]);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'below 10',
+ value: encodeValue(undefined, 10),
+ isRefined: false,
+ },
+ { label: '10 - 20', value: encodeValue(10, 20), isRefined: false },
+ { label: 'more than 20', value: encodeValue(20), isRefined: false },
+ ],
+ }),
+ expect.anything()
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -198,20 +254,24 @@ describe('connectNumericRefinementList', () => {
createURL: () => '#',
});
- const secondRenderingOptions = rendering.lastCall.args[0];
- expect(secondRenderingOptions.items).toEqual([
- {
- label: 'below 10',
- value: encodeValue(undefined, 10),
- isRefined: false,
- },
- { label: '10 - 20', value: encodeValue(10, 20), isRefined: false },
- { label: 'more than 20', value: encodeValue(20), isRefined: false },
- ]);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'below 10',
+ value: encodeValue(undefined, 10),
+ isRefined: false,
+ },
+ { label: '10 - 20', value: encodeValue(10, 20), isRefined: false },
+ { label: 'more than 20', value: encodeValue(20), isRefined: false },
+ ],
+ }),
+ expect.anything()
+ );
});
it('provides isRefined for the currently selected value', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectNumericRefinementList(rendering);
const listOptions = [
{ name: 'below 10', end: 10 },
@@ -226,7 +286,7 @@ describe('connectNumericRefinementList', () => {
});
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -235,7 +295,8 @@ describe('connectNumericRefinementList', () => {
onHistoryChange: () => {},
});
- let refine = rendering.lastCall.args[0].refine;
+ let refine =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0].refine;
listOptions.forEach((option, i) => {
refine(encodeValue(option.start, option.end));
@@ -254,7 +315,8 @@ describe('connectNumericRefinementList', () => {
// Then we modify the isRefined value of the one that is supposed to be refined
expectedResults[i].isRefined = true;
- const renderingParameters = rendering.lastCall.args[0];
+ const renderingParameters =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(renderingParameters.items).toEqual(expectedResults);
refine = renderingParameters.refine;
@@ -262,7 +324,7 @@ describe('connectNumericRefinementList', () => {
});
it('when the state is cleared, the "no value" value should be refined', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectNumericRefinementList(rendering);
const listOptions = [
{ name: 'below 10', end: 10 },
@@ -277,7 +339,7 @@ describe('connectNumericRefinementList', () => {
});
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -286,7 +348,8 @@ describe('connectNumericRefinementList', () => {
onHistoryChange: () => {},
});
- const refine = rendering.lastCall.args[0].refine;
+ const refine =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0].refine;
// a user selects a value in the refinement list
refine(encodeValue(listOptions[0].start, listOptions[0].end));
@@ -301,7 +364,8 @@ describe('connectNumericRefinementList', () => {
const expectedResults0 = [...listOptions].map(mapOptionsToItems);
expectedResults0[0].isRefined = true;
- const renderingParameters0 = rendering.lastCall.args[0];
+ const renderingParameters0 =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(renderingParameters0.items).toEqual(expectedResults0);
// All the refinements are cleared by a third party
@@ -318,7 +382,8 @@ describe('connectNumericRefinementList', () => {
const expectedResults1 = [...listOptions].map(mapOptionsToItems);
expectedResults1[4].isRefined = true;
- const renderingParameters1 = rendering.lastCall.args[0];
+ const renderingParameters1 =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(renderingParameters1.items).toEqual(expectedResults1);
});
@@ -382,7 +447,7 @@ describe('connectNumericRefinementList', () => {
});
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
helper.setPage(2);
widget.init({
diff --git a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js b/src/connectors/numeric-refinement-list/connectNumericRefinementList.js
index 18b36be164..7536cc1989 100644
--- a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js
+++ b/src/connectors/numeric-refinement-list/connectNumericRefinementList.js
@@ -17,6 +17,7 @@ search.addWidget(
customNumericRefinementList({
attributeName,
options,
+ transformItems,
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectNumericRefinementList.html
@@ -41,6 +42,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @typedef {Object} CustomNumericRefinementListWidgetOptions
* @property {string} attributeName Name of the attribute for filtering.
* @property {NumericRefinementListOption[]} options List of all the options.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -114,7 +116,11 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { attributeName, options } = widgetParams;
+ const {
+ attributeName,
+ options,
+ transformItems = items => items,
+ } = widgetParams;
if (!attributeName || !options) {
throw new Error(usage);
@@ -144,7 +150,7 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
renderFn(
{
createURL: this._createURL(helper.state),
- items: this._prepareItems(helper.state),
+ items: transformItems(this._prepareItems(helper.state)),
hasNoResults: true,
refine: this._refine,
instantSearchInstance,
@@ -158,7 +164,7 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
renderFn(
{
createURL: this._createURL(state),
- items: this._prepareItems(state),
+ items: transformItems(this._prepareItems(state)),
hasNoResults: results.nbHits === 0,
refine: this._refine,
instantSearchInstance,
diff --git a/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js b/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js
index d43e215a6f..beb3e9dbde 100644
--- a/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js
+++ b/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js
@@ -74,6 +74,55 @@ describe('connectNumericSelector', () => {
});
});
+ it('Renders during init and render with transformed items', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectNumericSelector(rendering);
+ const listOptions = [
+ { name: '10', value: 10 },
+ { name: '20', value: 20 },
+ { name: '30', value: 30 },
+ ];
+ const widget = makeWidget({
+ attributeName: 'numerics',
+ options: listOptions,
+ transformItems: items =>
+ items.map(item => ({ ...item, label: 'transformed' })),
+ });
+
+ const config = widget.getConfiguration({}, {});
+
+ const helper = jsHelper({}, '', config);
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.options).toEqual([
+ { name: '10', value: 10, label: 'transformed' },
+ { name: '20', value: 20, label: 'transformed' },
+ { name: '30', value: 30, label: 'transformed' },
+ ]);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{ nbHits: 0 }]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.options).toEqual([
+ { name: '10', value: 10, label: 'transformed' },
+ { name: '20', value: 20, label: 'transformed' },
+ { name: '30', value: 30, label: 'transformed' },
+ ]);
+ });
+
it('Reads the default value from the URL if possible', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
diff --git a/src/connectors/numeric-selector/connectNumericSelector.js b/src/connectors/numeric-selector/connectNumericSelector.js
index 33d94ad266..ab29a59a6e 100644
--- a/src/connectors/numeric-selector/connectNumericSelector.js
+++ b/src/connectors/numeric-selector/connectNumericSelector.js
@@ -15,7 +15,8 @@ search.addWidget(
customNumericSelector({
attributeName,
options,
- [ operator = '=' ]
+ [ operator = '=' ],
+ [ transformItems ]
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectNumericSelector.html
@@ -33,6 +34,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
* @property {NumericSelectorOption[]} options Array of objects defining the different values and labels.
* @property {string} [operator = '='] The operator to use to refine. Supports following operators: <, <=, =, >, >= and !=.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -98,7 +100,12 @@ export default function connectNumericSelector(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { attributeName, options, operator = '=' } = widgetParams;
+ const {
+ attributeName,
+ options,
+ operator = '=',
+ transformItems = items => items,
+ } = widgetParams;
if (!attributeName || !options) {
throw new Error(usage);
@@ -131,7 +138,7 @@ export default function connectNumericSelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: this._getRefinedValue(helper.state),
- options,
+ options: transformItems(options),
refine: this._refine,
hasNoResults: true,
instantSearchInstance,
@@ -145,7 +152,7 @@ export default function connectNumericSelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: this._getRefinedValue(helper.state),
- options,
+ options: transformItems(options),
refine: this._refine,
hasNoResults: results.nbHits === 0,
instantSearchInstance,
diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
index 201ce1bc93..11bc496276 100644
--- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
+++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
@@ -145,6 +145,69 @@ describe('connectRefinementList', () => {
});
});
+ it('transforms items if requested', () => {
+ const { makeWidget, rendering } = createWidgetFactory();
+ const widget = makeWidget({
+ attributeName: 'category',
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: 'transformed',
+ value: 'transformed',
+ highlighted: 'transformed',
+ })),
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.items).toEqual([]);
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.items).toEqual([
+ expect.objectContaining({
+ label: 'transformed',
+ value: 'transformed',
+ highlighted: 'transformed',
+ }),
+ expect.objectContaining({
+ label: 'transformed',
+ value: 'transformed',
+ highlighted: 'transformed',
+ }),
+ ]);
+ });
+
it('Provide a function to clear the refinements at each step', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
@@ -737,6 +800,110 @@ describe('connectRefinementList', () => {
});
});
+ it('can search in facet values with transformed items', () => {
+ const { makeWidget, rendering } = createWidgetFactory();
+ const widget = makeWidget({
+ attributeName: 'category',
+ limit: 2,
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: 'transformed',
+ value: 'transformed',
+ highlighted: 'transformed',
+ })),
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+ helper.searchForFacetValues = jest.fn().mockReturnValue(
+ Promise.resolve({
+ exhaustiveFacetsCount: true,
+ facetHits: [
+ {
+ count: 33,
+ highlighted: 'will be transformed',
+ value: 'will be transformed',
+ },
+ {
+ count: 9,
+ highlighted: 'will be transformed',
+ value: 'will be transformed',
+ },
+ ],
+ processingTimeMS: 1,
+ })
+ );
+
+ // Simulate the lifecycle
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+ expect(rendering).toHaveBeenCalledTimes(1);
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ c1: 880,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ c1: 880,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+ expect(rendering).toHaveBeenCalledTimes(2);
+ // Simulation end
+
+ const search = rendering.mock.calls[1][0].searchForItems;
+ search('transfo');
+
+ const [
+ sffvFacet,
+ sffvQuery,
+ maxNbItems,
+ paramOverride,
+ ] = helper.searchForFacetValues.mock.calls[0];
+
+ expect(sffvQuery).toBe('transfo');
+ expect(sffvFacet).toBe('category');
+ expect(maxNbItems).toBe(2);
+ expect(paramOverride).toEqual({
+ highlightPreTag: undefined,
+ highlightPostTag: undefined,
+ });
+
+ return Promise.resolve().then(() => {
+ expect(rendering).toHaveBeenCalledTimes(3);
+ expect(rendering.mock.calls[2][0].items).toEqual([
+ expect.objectContaining({
+ highlighted: 'transformed',
+ label: 'transformed',
+ value: 'transformed',
+ }),
+ expect.objectContaining({
+ highlighted: 'transformed',
+ label: 'transformed',
+ value: 'transformed',
+ }),
+ ]);
+ });
+ });
+
it('can search in facet values, and reset pre post tags if needed', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
diff --git a/src/connectors/refinement-list/connectRefinementList.js b/src/connectors/refinement-list/connectRefinementList.js
index 66bdc6dc12..6e16443157 100644
--- a/src/connectors/refinement-list/connectRefinementList.js
+++ b/src/connectors/refinement-list/connectRefinementList.js
@@ -25,7 +25,8 @@ search.addWidget(
[ limit ],
[ showMoreLimit ],
[ sortBy = ['isRefined', 'count:desc', 'name:asc'] ],
- [ escapeFacetValues = false ]
+ [ escapeFacetValues = false ],
+ [ transformItems ]
})
);
@@ -69,6 +70,7 @@ export const checkUsage = ({
* is showing more items.
* @property {string[]|function} [sortBy = ['isRefined', 'count:desc', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
* @property {boolean} [escapeFacetValues = false] Escapes the content of the facet values.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -159,6 +161,7 @@ export default function connectRefinementList(renderFn, unmountFn) {
showMoreLimit,
sortBy = ['isRefined', 'count:desc', 'name:asc'],
escapeFacetValues = false,
+ transformItems = items => items,
} = widgetParams;
checkUsage({
@@ -267,12 +270,12 @@ export default function connectRefinementList(renderFn, unmountFn) {
? escapeFacets(results.facetHits)
: results.facetHits;
- const normalizedFacetValues = facetValues.map(
- ({ value, ...item }) => ({
+ const normalizedFacetValues = transformItems(
+ facetValues.map(({ value, ...item }) => ({
...item,
value,
label: value,
- })
+ }))
);
render({
@@ -369,7 +372,9 @@ export default function connectRefinementList(renderFn, unmountFn) {
} = renderOptions;
const facetValues = results.getFacetValues(attributeName, { sortBy });
- const items = facetValues.slice(0, this.getLimit()).map(formatItems);
+ const items = transformItems(
+ facetValues.slice(0, this.getLimit()).map(formatItems)
+ );
const maxValuesPerFacetConfig = state.getQueryParameter(
'maxValuesPerFacet'
diff --git a/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js b/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js
index 4a8afaefd7..62ed795dfb 100644
--- a/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js
+++ b/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
@@ -12,13 +10,11 @@ describe('connectSortBySelector', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectSortBySelector(rendering);
const instantSearchInstance = instantSearch({
- apiKey: '',
- appId: '',
indexName: 'defaultIndex',
- createAlgoliaClient: () => ({}),
+ searchClient: { search() {} },
});
const indices = [
@@ -30,7 +26,7 @@ describe('connectSortBySelector', () => {
expect(widget.getConfiguration).toBe(undefined);
const helper = jsHelper({}, indices[0].name);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -40,25 +36,19 @@ describe('connectSortBySelector', () => {
instantSearchInstance,
});
- {
- // should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
- expect(isFirstRendering).toBe(true);
-
- // should provide good values for the first rendering
- const {
- currentRefinement,
- options,
- widgetParams,
- } = rendering.lastCall.args[0];
- expect(currentRefinement).toBe(helper.state.index);
- expect(widgetParams).toEqual({ indices });
- expect(options).toEqual([
- { label: 'Sort products by relevance', value: 'relevance' },
- { label: 'Sort products by price', value: 'priceASC' },
- ]);
- }
+ // should call the rendering once with isFirstRendering to true
+ expect(rendering).toHaveBeenCalledTimes(1);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ currentRefinement: helper.state.index,
+ widgetParams: { indices },
+ options: [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
+ ],
+ }),
+ true
+ );
widget.render({
results: new SearchResults(helper.state, [{}]),
@@ -67,30 +57,82 @@ describe('connectSortBySelector', () => {
createURL: () => '#',
});
- {
- // Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
- expect(isFirstRendering).toBe(false);
+ // Should call the rendering a second time, with isFirstRendering to false
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ currentRefinement: helper.state.index,
+ widgetParams: { indices },
+ options: [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
+ ],
+ }),
+ false
+ );
+ });
- // should provide good values after the first search
- const { currentRefinement, options } = rendering.lastCall.args[0];
- expect(currentRefinement).toBe(helper.state.index);
- expect(options).toEqual([
- { label: 'Sort products by relevance', value: 'relevance' },
- { label: 'Sort products by price', value: 'priceASC' },
- ]);
- }
+ it('Renders with transformed items', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectSortBySelector(rendering);
+ const instantSearchInstance = instantSearch({
+ indexName: 'defaultIndex',
+ searchClient: { search() {} },
+ });
+
+ const indices = [
+ { label: 'Sort products by relevance', name: 'relevance' },
+ { label: 'Sort products by price', name: 'priceASC' },
+ ];
+ const widget = makeWidget({
+ indices,
+ transformItems: items =>
+ items.map(item => ({ ...item, label: 'transformed' })),
+ });
+
+ const helper = jsHelper({}, indices[0].name);
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ instantSearchInstance,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ options: [
+ { label: 'transformed', value: 'relevance' },
+ { label: 'transformed', value: 'priceASC' },
+ ],
+ }),
+ expect.anything()
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ helper,
+ state: helper.state,
+ instantSearchInstance,
+ });
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ options: [
+ { label: 'transformed', value: 'relevance' },
+ { label: 'transformed', value: 'priceASC' },
+ ],
+ }),
+ expect.anything()
+ );
});
it('Provides a function to update the index at each step', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectSortBySelector(rendering);
const instantSearchInstance = instantSearch({
- apiKey: '',
- appId: '',
indexName: 'defaultIndex',
- createAlgoliaClient: () => ({}),
+ searchClient: { search() {} },
});
const indices = [
@@ -102,7 +144,7 @@ describe('connectSortBySelector', () => {
});
const helper = jsHelper({}, indices[0].name);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -115,12 +157,13 @@ describe('connectSortBySelector', () => {
{
// first rendering
expect(helper.state.index).toBe(indices[0].name);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, currentRefinement } = renderOptions;
expect(currentRefinement).toBe(helper.state.index);
refine('bip');
expect(helper.state.index).toBe('bip');
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
}
widget.render({
@@ -133,12 +176,13 @@ describe('connectSortBySelector', () => {
{
// Second rendering
expect(helper.state.index).toBe('bip');
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, currentRefinement } = renderOptions;
expect(currentRefinement).toBe('bip');
refine('bop');
expect(helper.state.index).toBe('bop');
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
}
});
@@ -147,10 +191,8 @@ describe('connectSortBySelector', () => {
const rendering = jest.fn();
const makeWidget = connectSortBySelector(rendering);
const instantSearchInstance = instantSearch({
- apiKey: '',
- appId: '',
indexName: 'relevance',
- createAlgoliaClient: () => ({}),
+ searchClient: { search() {} },
});
const indices = [
{ label: 'Sort products by relevance', name: 'relevance' },
diff --git a/src/connectors/sort-by-selector/connectSortBySelector.js b/src/connectors/sort-by-selector/connectSortBySelector.js
index 24f00c2425..fd959abab3 100644
--- a/src/connectors/sort-by-selector/connectSortBySelector.js
+++ b/src/connectors/sort-by-selector/connectSortBySelector.js
@@ -13,7 +13,10 @@ var customSortBySelector = connectSortBySelector(function render(params, isFirst
// }
});
search.addWidget(
- customSortBySelector({ indices })
+ customSortBySelector({
+ indices,
+ [ transformItems ]
+ })
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectSortBySelector.html
`;
@@ -27,6 +30,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomSortBySelectorWidgetOptions
* @property {SortBySelectorIndices[]} indices Array of objects defining the different indices to choose from.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -98,7 +102,7 @@ export default function connectSortBySelector(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { indices } = widgetParams;
+ const { indices, transformItems = items => items } = widgetParams;
if (!indices) {
throw new Error(usage);
@@ -129,7 +133,7 @@ export default function connectSortBySelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: currentIndex,
- options: selectorOptions,
+ options: transformItems(selectorOptions),
refine: this.setIndex,
hasNoResults: true,
widgetParams,
@@ -143,7 +147,7 @@ export default function connectSortBySelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: helper.getIndex(),
- options: selectorOptions,
+ options: transformItems(selectorOptions),
refine: this.setIndex,
hasNoResults: results.nbHits === 0,
widgetParams,
diff --git a/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap b/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap
new file mode 100644
index 0000000000..cf3d1f4857
--- /dev/null
+++ b/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`breadcrumb() render renders transformed items correctly 1`] = `
+ Digital Cameras",
+ },
+ Object {
+ "name": "Digital Cameras",
+ "transformed": true,
+ "value": null,
+ },
+ ]
+ }
+ refine={[Function]}
+ separator=" > "
+ shouldAutoHideContainer={false}
+ templateProps={
+ Object {
+ "templates": Object {
+ "home": "Home",
+ "separator": "",
+ },
+ "templatesConfig": undefined,
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "home": false,
+ "separator": false,
+ },
+ }
+ }
+/>
+`;
diff --git a/src/widgets/breadcrumb/__tests__/breadcrumb-test.js b/src/widgets/breadcrumb/__tests__/breadcrumb-test.js
new file mode 100644
index 0000000000..bd8175fb3a
--- /dev/null
+++ b/src/widgets/breadcrumb/__tests__/breadcrumb-test.js
@@ -0,0 +1,97 @@
+import breadcrumb from '../breadcrumb';
+
+describe('breadcrumb()', () => {
+ let container;
+ let attributes;
+ let ReactDOM;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ attributes = ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1'];
+ ReactDOM = { render: jest.fn() };
+ breadcrumb.__Rewire__('render', ReactDOM.render);
+ });
+
+ describe('render', () => {
+ let results;
+ let helper;
+ let state;
+
+ beforeEach(() => {
+ const data = [
+ {
+ name: 'Cameras & Camcorders',
+ path: 'Cameras & Camcorders',
+ count: 1369,
+ isRefined: true,
+ data: [
+ {
+ name: 'Digital Cameras',
+ path: 'Cameras & Camcorders > Digital Cameras',
+ count: 170,
+ isRefined: true,
+ data: null,
+ },
+ ],
+ },
+ ];
+
+ results = {
+ getFacetValues: jest.fn(() => ({ data })),
+ hierarchicalFacets: [
+ {
+ name: 'hierarchicalCategories.lvl0',
+ count: null,
+ isRefined: true,
+ path: null,
+ data,
+ },
+ ],
+ };
+
+ helper = {
+ search: jest.fn(),
+ toggleRefinement: jest.fn().mockReturnThis(),
+ };
+
+ state = {
+ toggleRefinement: jest.fn().mockReturnThis(),
+ hierarchicalFacets: [
+ {
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ ],
+ name: 'hierarchicalCategories.lvl0',
+ separator: ' > ',
+ rootPath: null,
+ },
+ ],
+ };
+ });
+
+ it('renders transformed items correctly', () => {
+ const widget = breadcrumb({
+ container,
+ attributes,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+ widget.init({
+ helper,
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results,
+ state,
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ afterEach(() => {
+ breadcrumb.__ResetDependency__('render');
+ });
+ });
+});
diff --git a/src/widgets/breadcrumb/breadcrumb.js b/src/widgets/breadcrumb/breadcrumb.js
index f46f5b4c47..fd61b449f5 100644
--- a/src/widgets/breadcrumb/breadcrumb.js
+++ b/src/widgets/breadcrumb/breadcrumb.js
@@ -60,6 +60,7 @@ breadcrumb({
[ cssClasses.{disabledLabel, home, label, root, separator}={} ],
[ templates.{home, separator}]
[ transformData.{item} ],
+ [ transformItems ],
})`;
/**
@@ -92,6 +93,7 @@ breadcrumb({
* @property {BreadcrumbTransforms} [transformData] Set of functions to transform the data passed to the templates.
* @property {boolean} [autoHideContainer=true] Hides the container when there are no items in the breadcrumb.
* @property {BreadcrumbCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -156,6 +158,7 @@ export default function breadcrumb({
separator = ' > ',
templates = defaultTemplates,
transformData,
+ transformItems,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -186,7 +189,7 @@ export default function breadcrumb({
const makeBreadcrumb = connectBreadcrumb(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeBreadcrumb({ attributes, rootPath });
+ return makeBreadcrumb({ attributes, rootPath, transformItems });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap b/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap
index 9eef79fb7e..409fdc1bcc 100644
--- a/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap
+++ b/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap
@@ -3070,6 +3070,223 @@ exports[`currentRefinedValues() render() options.templates should pass it in tem
/>
`;
+exports[`currentRefinedValues() render() options.transformItems should transform passed items 1`] = `
+=",
+ "transformed": true,
+ "type": "numeric",
+ },
+ Object {
+ "attributeName": "numericFacet",
+ "computedLabel": "≤ 2",
+ "name": "2",
+ "numericValue": 2,
+ "operator": "<=",
+ "transformed": true,
+ "type": "numeric",
+ },
+ Object {
+ "attributeName": "numericDisjunctiveFacet",
+ "computedLabel": "≥ 3",
+ "name": "3",
+ "numericValue": 3,
+ "operator": ">=",
+ "transformed": true,
+ "type": "numeric",
+ },
+ Object {
+ "attributeName": "numericDisjunctiveFacet",
+ "computedLabel": "≤ 4",
+ "name": "4",
+ "numericValue": 4,
+ "operator": "<=",
+ "transformed": true,
+ "type": "numeric",
+ },
+ Object {
+ "attributeName": "_tags",
+ "computedLabel": "tag1",
+ "name": "tag1",
+ "transformed": true,
+ "type": "tag",
+ },
+ Object {
+ "attributeName": "_tags",
+ "computedLabel": "tag2",
+ "name": "tag2",
+ "transformed": true,
+ "type": "tag",
+ },
+ ]
+ }
+ shouldAutoHideContainer={false}
+ templateProps={
+ Object {
+ "templates": Object {
+ "clearAll": "CLEAR ALL",
+ "footer": "FOOTER",
+ "header": "HEADER",
+ "item": "ITEM",
+ },
+ "templatesConfig": Object {
+ "randomAttributeNeverUsed": "value",
+ },
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "clearAll": true,
+ "footer": true,
+ "header": true,
+ "item": true,
+ },
+ }
+ }
+/>
+`;
+
exports[`currentRefinedValues() render() should render twice 1`] = `
{
function setRefinementsInExpectedProps() {
expectedProps.refinements = refinements;
- expectedProps.clearRefinementClicks = map(refinements, () => () => {});
- expectedProps.clearRefinementURLs = map(refinements, () => '#cleared');
+ expectedProps.clearRefinementClicks = refinements.map(() => () => {});
+ expectedProps.clearRefinementURLs = refinements.map(() => '#cleared');
}
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
currentRefinedValues.__Rewire__('render', ReactDOM.render);
parameters = {
@@ -634,11 +631,11 @@ describe('currentRefinedValues()', () => {
widget.render(renderParameters);
widget.render(renderParameters);
- expect(ReactDOM.render.callCount).toBe(2);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toBe(parameters.container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toBe(parameters.container);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(parameters.container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toBe(parameters.container);
});
describe('options.container', () => {
@@ -652,9 +649,9 @@ describe('currentRefinedValues()', () => {
const widget = currentRefinedValues(parameters);
widget.init(initParameters);
widget.render(renderParameters);
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toBe(element);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(element);
});
it('should render with a HTMLElement container', () => {
@@ -665,9 +662,9 @@ describe('currentRefinedValues()', () => {
const widget = currentRefinedValues(parameters);
widget.init(initParameters);
widget.render(renderParameters);
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toBe(element);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(element);
});
});
@@ -695,8 +692,8 @@ describe('currentRefinedValues()', () => {
setRefinementsInExpectedProps();
expectedProps.attributes = {};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('should render all attributes with an empty array', () => {
@@ -717,8 +714,8 @@ describe('currentRefinedValues()', () => {
setRefinementsInExpectedProps();
expectedProps.attributes = {};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('should render and pass all attributes defined in each objects', () => {
@@ -739,8 +736,7 @@ describe('currentRefinedValues()', () => {
},
];
- refinements = filter(
- refinements,
+ refinements = refinements.filter(
refinement =>
['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
refinement.attributeName
@@ -769,8 +765,8 @@ describe('currentRefinedValues()', () => {
},
};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -797,8 +793,8 @@ describe('currentRefinedValues()', () => {
setRefinementsInExpectedProps();
expectedProps.attributes = {};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('should render all attributes with an empty array', () => {
@@ -819,8 +815,8 @@ describe('currentRefinedValues()', () => {
setRefinementsInExpectedProps();
expectedProps.attributes = {};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('should render and pass all attributes defined in each objects', () => {
@@ -848,15 +844,13 @@ describe('currentRefinedValues()', () => {
count: 42,
exhaustive: true,
});
- const firstRefinements = filter(
- refinements,
+ const firstRefinements = refinements.filter(
refinement =>
['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
refinement.attributeName
) !== -1
);
- const otherRefinements = filter(
- refinements,
+ const otherRefinements = refinements.filter(
refinement =>
['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
refinement.attributeName
@@ -886,8 +880,8 @@ describe('currentRefinedValues()', () => {
},
};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -921,15 +915,13 @@ describe('currentRefinedValues()', () => {
count: 42,
exhaustive: true,
});
- const firstRefinements = filter(
- refinements,
+ const firstRefinements = refinements.filter(
refinement =>
['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
refinement.attributeName
) !== -1
);
- const otherRefinements = filter(
- refinements,
+ const otherRefinements = refinements.filter(
refinement =>
['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
refinement.attributeName
@@ -959,8 +951,8 @@ describe('currentRefinedValues()', () => {
},
};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
});
@@ -975,8 +967,8 @@ describe('currentRefinedValues()', () => {
expectedProps.clearAllPosition = 'before';
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -990,8 +982,24 @@ describe('currentRefinedValues()', () => {
expectedProps.templateProps.templates.item = 'MY CUSTOM TEMPLATE';
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+ });
+
+ describe('options.transformItems', () => {
+ it('should transform passed items', () => {
+ const widget = currentRefinedValues({
+ ...parameters,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init(initParameters);
+ widget.render(renderParameters);
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -1014,8 +1022,8 @@ describe('currentRefinedValues()', () => {
widget.init(initParameters);
widget.render(renderParameters);
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('shouldAutoHideContainer should be false with autoHideContainer = false', () => {
@@ -1028,8 +1036,8 @@ describe('currentRefinedValues()', () => {
expectedProps.shouldAutoHideContainer = false;
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -1043,8 +1051,8 @@ describe('currentRefinedValues()', () => {
expectedProps.shouldAutoHideContainer = false;
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('shouldAutoHideContainer should be false with autoHideContainer = false', () => {
@@ -1056,8 +1064,8 @@ describe('currentRefinedValues()', () => {
expectedProps.shouldAutoHideContainer = false;
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
});
@@ -1073,8 +1081,8 @@ describe('currentRefinedValues()', () => {
expectedProps.cssClasses.body =
'ais-current-refined-values--body custom-passed-body';
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('should work with an array', () => {
@@ -1087,8 +1095,8 @@ describe('currentRefinedValues()', () => {
expectedProps.cssClasses.body =
'ais-current-refined-values--body custom-body custom-body-2';
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
@@ -1107,19 +1115,19 @@ describe('currentRefinedValues()', () => {
count: 42,
exhaustive: true,
});
- const firstRefinements = filter(refinements, {
- attributeName: 'disjunctiveFacet',
- });
- const secondRefinements = filter(refinements, {
- attributeName: 'facetExclude',
- });
- const otherRefinements = filter(
- refinements,
+ const firstRefinements = refinements.filter(
+ refinement => refinement.attributeName === 'disjunctiveFacet'
+ );
+ const secondRefinements = refinements.filter(
+ refinement => refinement.attributeName === 'facetExclude'
+ );
+ const otherRefinements = refinements.filter(
refinement =>
- ['disjunctiveFacet', 'facetExclude'].indexOf(
+ !['disjunctiveFacet', 'facetExclude'].includes(
refinement.attributeName
- ) === -1
+ )
);
+
refinements = []
.concat(firstRefinements)
.concat(secondRefinements)
@@ -1135,8 +1143,8 @@ describe('currentRefinedValues()', () => {
facetExclude: { name: 'facetExclude' },
};
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
});
diff --git a/src/widgets/current-refined-values/current-refined-values.js b/src/widgets/current-refined-values/current-refined-values.js
index 06016874a6..bc02ff0d5c 100644
--- a/src/widgets/current-refined-values/current-refined-values.js
+++ b/src/widgets/current-refined-values/current-refined-values.js
@@ -91,8 +91,9 @@ currentRefinedValues({
[ transformData.{item} ],
[ autoHideContainer = true ],
[ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ],
- [ collapsible = false ]
- [ clearsQuery = false ]
+ [ collapsible = false ],
+ [ clearsQuery = false ],
+ [ transformItems ]
})`;
/**
@@ -148,6 +149,7 @@ currentRefinedValues({
* choose to hide the content of the widget. This option can also be an object with the property collapsed. If this
* property is `true`, then the widget is hidden during the first rendering.
* @property {boolean} [clearsQuery=false] If true, the clear all button also clears the active search query.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -189,6 +191,7 @@ export default function currentRefinedValues({
cssClasses: userCssClasses = {},
collapsible = false,
clearsQuery = false,
+ transformItems,
}) {
const transformDataOK =
isUndefined(transformData) ||
@@ -278,6 +281,7 @@ export default function currentRefinedValues({
onlyListedAttributes,
clearAll,
clearsQuery,
+ transformItems,
});
} catch (e) {
throw new Error(usage);
diff --git a/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap b/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
index 17973c2050..84995fe448 100644
--- a/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
+++ b/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
@@ -102,6 +102,59 @@ exports[`hierarchicalMenu() render has a templates option 1`] = `
/>
`;
+exports[`hierarchicalMenu() render has a transformItems options 1`] = `
+{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}",
+ },
+ "templatesConfig": undefined,
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "footer": false,
+ "header": false,
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+/>
+`;
+
exports[`hierarchicalMenu() render sets facetValues to empty array when no results 1`] = `
{
@@ -12,7 +11,7 @@ describe('hierarchicalMenu()', () => {
container = document.createElement('div');
attributes = ['hello', 'world'];
options = {};
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
hierarchicalMenu.__Rewire__('render', ReactDOM.render);
});
@@ -139,13 +138,13 @@ describe('hierarchicalMenu()', () => {
beforeEach(() => {
data = { data: [{ name: 'foo' }, { name: 'bar' }] };
- results = { getFacetValues: sinon.spy(() => data) };
+ results = { getFacetValues: jest.fn(() => data) };
helper = {
- toggleRefinement: sinon.stub().returnsThis(),
- search: sinon.spy(),
+ toggleRefinement: jest.fn().mockReturnThis(),
+ search: jest.fn(),
};
state = {
- toggleRefinement: sinon.spy(),
+ toggleRefinement: jest.fn(),
};
options = { container, attributes };
createURL = () => '#';
@@ -167,41 +166,35 @@ describe('hierarchicalMenu()', () => {
widget = hierarchicalMenu({ ...options, cssClasses: userCssClasses });
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('calls ReactDOM.render', () => {
widget = hierarchicalMenu(options);
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(ReactDOM.render.calledOnce).toBe(true);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('asks for results.getFacetValues', () => {
widget = hierarchicalMenu(options);
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(results.getFacetValues.calledOnce).toBe(true);
- expect(results.getFacetValues.getCall(0).args).toEqual([
- 'hello',
- {
- sortBy: ['name:asc'],
- },
- ]);
+ expect(results.getFacetValues).toHaveBeenCalledTimes(1);
+ expect(results.getFacetValues).toHaveBeenCalledWith('hello', {
+ sortBy: ['name:asc'],
+ });
});
it('has a sortBy option', () => {
widget = hierarchicalMenu({ ...options, sortBy: ['count:asc'] });
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(results.getFacetValues.calledOnce).toBe(true);
- expect(results.getFacetValues.getCall(0).args).toEqual([
- 'hello',
- {
- sortBy: ['count:asc'],
- },
- ]);
+ expect(results.getFacetValues).toHaveBeenCalledTimes(1);
+ expect(results.getFacetValues).toHaveBeenCalledWith('hello', {
+ sortBy: ['count:asc'],
+ });
});
it('has a templates option', () => {
@@ -215,7 +208,20 @@ describe('hierarchicalMenu()', () => {
});
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ it('has a transformItems options', () => {
+ widget = hierarchicalMenu({
+ ...options,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, createURL, instantSearchInstance: {} });
+ widget.render({ results, state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('sets shouldAutoHideContainer to true when no results', () => {
@@ -223,7 +229,7 @@ describe('hierarchicalMenu()', () => {
widget = hierarchicalMenu(options);
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('sets facetValues to empty array when no results', () => {
@@ -231,7 +237,7 @@ describe('hierarchicalMenu()', () => {
widget = hierarchicalMenu(options);
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('has a toggleRefinement method', () => {
@@ -239,12 +245,11 @@ describe('hierarchicalMenu()', () => {
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
const elementToggleRefinement =
- ReactDOM.render.firstCall.args[0].props.toggleRefinement;
+ ReactDOM.render.mock.calls[0][0].props.toggleRefinement;
elementToggleRefinement('mom');
- expect(helper.toggleRefinement.calledOnce).toBe(true);
- expect(helper.toggleRefinement.getCall(0).args).toEqual(['hello', 'mom']);
- expect(helper.search.calledOnce).toBe(true);
- expect(helper.toggleRefinement.calledBefore(helper.search)).toBe(true);
+ expect(helper.toggleRefinement).toHaveBeenCalledTimes(1);
+ expect(helper.toggleRefinement).toHaveBeenCalledWith('hello', 'mom');
+ expect(helper.search).toHaveBeenCalledTimes(1);
});
it('has a limit option', () => {
@@ -281,7 +286,7 @@ describe('hierarchicalMenu()', () => {
widget.init({ helper, createURL, instantSearchInstance: {} });
widget.render({ results, state });
const actualFacetValues =
- ReactDOM.render.firstCall.args[0].props.facetValues;
+ ReactDOM.render.mock.calls[0][0].props.facetValues;
expect(actualFacetValues).toEqual(expectedFacetValues);
});
diff --git a/src/widgets/hierarchical-menu/hierarchical-menu.js b/src/widgets/hierarchical-menu/hierarchical-menu.js
index d6aa62eb34..cad20eb166 100644
--- a/src/widgets/hierarchical-menu/hierarchical-menu.js
+++ b/src/widgets/hierarchical-menu/hierarchical-menu.js
@@ -64,7 +64,8 @@ hierarchicalMenu({
[ templates.{header, item, footer} ],
[ transformData.{item} ],
[ autoHideContainer=true ],
- [ collapsible=false ]
+ [ collapsible=false ],
+ [ transformItems ]
})`;
/**
* @typedef {Object} HierarchicalMenuCSSClasses
@@ -131,6 +132,7 @@ hierarchicalMenu({
* @property {boolean|{collapsed: boolean}} [collapsible=false] Makes the widget collapsible. The user can then
* choose to hide the content of the widget. This option can also be an object with the property collapsed. If this
* property is `true`, then the widget is hidden during the first rendering.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -200,6 +202,7 @@ export default function hierarchicalMenu({
templates = defaultTemplates,
collapsible = false,
transformData,
+ transformItems,
} = {}) {
if (!container || !attributes || !attributes.length) {
throw new Error(usage);
@@ -242,6 +245,7 @@ export default function hierarchicalMenu({
showParentLevel,
limit,
sortBy,
+ transformItems,
});
} catch (e) {
throw new Error(usage);
diff --git a/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap b/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap
index ac6c46d76f..55d5cb941a 100644
--- a/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap
+++ b/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap
@@ -28,3 +28,35 @@ exports[`hitsPerPageSelector() calls twice ReactDOM.render(, c
shouldAutoHideContainer={false}
/>
`;
+
+exports[`hitsPerPageSelector() renders transformed items 1`] = `
+
+`;
diff --git a/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js b/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js
index d0044e333f..71f02dd55a 100644
--- a/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js
+++ b/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js
@@ -1,4 +1,3 @@
-import sinon from 'sinon';
import hitsPerPageSelector from '../hits-per-page-selector';
describe('hitsPerPageSelector call', () => {
@@ -25,10 +24,10 @@ describe('hitsPerPageSelector()', () => {
let state;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
hitsPerPageSelector.__Rewire__('render', ReactDOM.render);
- consoleWarn = sinon.stub(window.console, 'warn');
+ consoleWarn = jest.spyOn(window.console, 'warn');
container = document.createElement('div');
items = [
@@ -45,8 +44,8 @@ describe('hitsPerPageSelector()', () => {
state: {
hitsPerPage: 20,
},
- setQueryParameter: sinon.stub().returnsThis(),
- search: sinon.spy(),
+ setQueryParameter: jest.fn().mockReturnThis(),
+ search: jest.fn(),
};
state = {
hitsPerPage: 10,
@@ -80,26 +79,43 @@ describe('hitsPerPageSelector()', () => {
widget.init({ helper, state: helper.state });
widget.render({ results, state });
widget.render({ results, state });
- expect(ReactDOM.render.callCount).toBe(2);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ it('renders transformed items', () => {
+ widget = hitsPerPageSelector({
+ container,
+ items: [
+ { value: 10, label: '10 results' },
+ { value: 20, label: '20 results', default: true },
+ ],
+ transformItems: widgetItems =>
+ widgetItems.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, state: helper.state });
+ widget.render({ results, state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('sets the underlying hitsPerPage', () => {
widget.init({ helper, state: helper.state });
widget.setHitsPerPage(helper, helper.state, 10);
- expect(helper.setQueryParameter.calledOnce).toBe(
- true,
+ expect(helper.setQueryParameter).toHaveBeenCalledTimes(
+ 1,
'setQueryParameter called once'
);
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('should throw if there is no name attribute in a passed object', () => {
items.length = 0;
items.push({ label: 'Label without a value' });
widget.init({ state: helper.state, helper });
- expect(consoleWarn.calledOnce).toBe(true, 'console.warn called once');
- expect(consoleWarn.firstCall.args[0]).toEqual(
+ expect(consoleWarn).toHaveBeenCalledTimes(1, 'console.warn called once');
+ expect(consoleWarn.mock.calls[0][0]).toEqual(
`[Warning][hitsPerPageSelector] No item in \`items\`
with \`value: hitsPerPage\` (hitsPerPage: 20)`
);
@@ -108,8 +124,8 @@ describe('hitsPerPageSelector()', () => {
it('must include the current hitsPerPage at initialization time', () => {
helper.state.hitsPerPage = -1;
widget.init({ state: helper.state, helper });
- expect(consoleWarn.calledOnce).toBe(true, 'console.warn called once');
- expect(consoleWarn.firstCall.args[0]).toEqual(
+ expect(consoleWarn).toHaveBeenCalledTimes(1, 'console.warn called once');
+ expect(consoleWarn.mock.calls[0][0]).toEqual(
`[Warning][hitsPerPageSelector] No item in \`items\`
with \`value: hitsPerPage\` (hitsPerPage: -1)`
);
@@ -124,6 +140,6 @@ describe('hitsPerPageSelector()', () => {
afterEach(() => {
hitsPerPageSelector.__ResetDependency__('render');
- consoleWarn.restore();
+ consoleWarn.mockRestore();
});
});
diff --git a/src/widgets/hits-per-page-selector/hits-per-page-selector.js b/src/widgets/hits-per-page-selector/hits-per-page-selector.js
index 607890443e..85cbe5e794 100644
--- a/src/widgets/hits-per-page-selector/hits-per-page-selector.js
+++ b/src/widgets/hits-per-page-selector/hits-per-page-selector.js
@@ -36,7 +36,8 @@ hitsPerPageSelector({
container,
items,
[ cssClasses.{root,select,item}={} ],
- [ autoHideContainer=false ]
+ [ autoHideContainer=false ],
+ [ transformItems ]
})`;
/**
@@ -59,6 +60,7 @@ hitsPerPageSelector({
* @property {HitsPerPageSelectorItems[]} items Array of objects defining the different values and labels.
* @property {boolean} [autoHideContainer=false] Hide the container when no results match.
* @property {HitsPerPageSelectorCSSClasses} [cssClasses] CSS classes to be added.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -88,6 +90,7 @@ export default function hitsPerPageSelector({
items,
cssClasses: userCssClasses = {},
autoHideContainer = false,
+ transformItems,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -114,7 +117,7 @@ export default function hitsPerPageSelector({
specializedRenderer,
() => unmountComponentAtNode(containerNode)
);
- return makeHitsPerPageSelector({ items });
+ return makeHitsPerPageSelector({ items, transformItems });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap b/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
index a7ec3df67c..36929b4314 100644
--- a/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
+++ b/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
@@ -87,3 +87,49 @@ exports[`hits() calls twice ReactDOM.render(, container) 2`] = `
}
/>
`;
+
+exports[`hits() renders transformed items 1`] = `
+
+`;
diff --git a/src/widgets/hits/__tests__/hits-test.js b/src/widgets/hits/__tests__/hits-test.js
index c2381f2cb3..eec054473f 100644
--- a/src/widgets/hits/__tests__/hits-test.js
+++ b/src/widgets/hits/__tests__/hits-test.js
@@ -1,5 +1,3 @@
-import expect from 'expect';
-import sinon from 'sinon';
import hits from '../hits.js';
import defaultTemplates from '../defaultTemplates.js';
@@ -17,7 +15,7 @@ describe('hits()', () => {
let results;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
hits.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
@@ -36,11 +34,24 @@ describe('hits()', () => {
widget.render({ results });
widget.render({ results });
- expect(ReactDOM.render.callCount).toBe(2);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+
+ it('renders transformed items', () => {
+ widget = hits({
+ container,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ instantSearchInstance: {} });
+ widget.render({ results });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('does not accept both item and allItems templates', () => {
diff --git a/src/widgets/hits/hits.js b/src/widgets/hits/hits.js
index 8636055e25..e994b7593e 100644
--- a/src/widgets/hits/hits.js
+++ b/src/widgets/hits/hits.js
@@ -47,6 +47,7 @@ const renderer = ({
const usage = `Usage:
hits({
container,
+ [ transformItems ],
[ cssClasses.{root,empty,item}={} ],
[ templates.{empty,item} | templates.{empty, allItems} ],
[ transformData.{empty,item} | transformData.{empty, allItems} ],
@@ -80,6 +81,7 @@ hits({
* @property {HitsTransforms} [transformData] Method to change the object passed to the templates.
* @property {HitsCSSClasses} [cssClasses] CSS classes to add.
* @property {boolean} [escapeHits = false] Escape HTML entities from hits string values.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -102,6 +104,7 @@ hits({
* item: 'Hit {{objectID}}: {{{_highlightResult.name.value}}}'
* },
* escapeHits: true,
+ * transformItems: items => items.map(item => item),
* })
* );
*/
@@ -111,6 +114,7 @@ export default function hits({
templates = defaultTemplates,
transformData,
escapeHits = false,
+ transformItems,
}) {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
@@ -139,7 +143,7 @@ export default function hits({
const makeHits = connectHits(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeHits({ escapeHits });
+ return makeHits({ escapeHits, transformItems });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap b/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
index e44e64a2ea..9d3fb13117 100644
--- a/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
+++ b/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
@@ -199,3 +199,54 @@ exports[`infiniteHits() if it is the last page, then the props should contain is
}
/>
`;
+
+exports[`infiniteHits() renders transformed items 1`] = `
+
+`;
diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-test.js b/src/widgets/infinite-hits/__tests__/infinite-hits-test.js
index e95cc4bafe..f968b24c07 100644
--- a/src/widgets/infinite-hits/__tests__/infinite-hits-test.js
+++ b/src/widgets/infinite-hits/__tests__/infinite-hits-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-import expect from 'expect';
import algoliasearchHelper from 'algoliasearch-helper';
import infiniteHits from '../infinite-hits';
@@ -18,9 +16,9 @@ describe('infiniteHits()', () => {
beforeEach(() => {
helper = algoliasearchHelper({});
- helper.search = sinon.spy();
+ helper.search = jest.fn();
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
infiniteHits.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
@@ -45,14 +43,32 @@ describe('infiniteHits()', () => {
widget.render({ results, state });
widget.render({ results, state });
- expect(ReactDOM.render.calledTwice).toBe(
- true,
+ expect(ReactDOM.render).toHaveBeenCalledTimes(
+ 2,
'ReactDOM.render called twice'
);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+
+ it('renders transformed items', () => {
+ const state = { page: 0 };
+ widget = infiniteHits({
+ container,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, instantSearchInstance: {} });
+ widget.render({
+ results,
+ state,
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('if it is the last page, then the props should contain isLastPage true', () => {
@@ -66,14 +82,14 @@ describe('infiniteHits()', () => {
state,
});
- expect(ReactDOM.render.calledTwice).toBe(
- true,
+ expect(ReactDOM.render).toHaveBeenCalledTimes(
+ 2,
'ReactDOM.render called twice'
);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
});
it('does not accept allItems templates', () => {
@@ -96,7 +112,7 @@ describe('infiniteHits()', () => {
widget.showMore();
expect(helper.state.page).toBe(1);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
});
afterEach(() => {
diff --git a/src/widgets/infinite-hits/infinite-hits.js b/src/widgets/infinite-hits/infinite-hits.js
index cb282081f6..01fe1dd2c7 100644
--- a/src/widgets/infinite-hits/infinite-hits.js
+++ b/src/widgets/infinite-hits/infinite-hits.js
@@ -53,6 +53,7 @@ Usage:
infiniteHits({
container,
[ escapeHits = false ],
+ [ transformItems ],
[ showMoreLabel ],
[ cssClasses.{root,empty,item,showmore,showmoreButton}={} ],
[ templates.{empty,item} | templates.{empty} ],
@@ -88,6 +89,7 @@ infiniteHits({
* @property {InfiniteHitsTransforms} [transformData] Method to change the object passed to the templates.
* @property {InfiniteHitsCSSClasses} [cssClasses] CSS classes to add.
* @property {boolean} [escapeHits = false] Escape HTML entities from hits string values.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -110,6 +112,7 @@ infiniteHits({
* item: 'Hit {{objectID}}: {{{_highlightResult.name.value}}}'
* },
* escapeHits: true,
+ * transformItems: items => items.map(item => item),
* })
* );
*/
@@ -120,6 +123,7 @@ export default function infiniteHits({
templates = defaultTemplates,
transformData,
escapeHits = false,
+ transformItems,
} = {}) {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
@@ -155,7 +159,7 @@ export default function infiniteHits({
const makeInfiniteHits = connectInfiniteHits(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeInfiniteHits({ escapeHits });
+ return makeInfiniteHits({ escapeHits, transformItems });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap b/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
index 4896e97392..9dc10401dc 100644
--- a/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
+++ b/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`menuSelect render correctly 1`] = `
+exports[`menuSelect render renders correctly 1`] = `
`;
+
+exports[`menuSelect render renders transformed items correctly 1`] = `
+
+`;
diff --git a/src/widgets/menu-select/__tests__/menu-select-test.js b/src/widgets/menu-select/__tests__/menu-select-test.js
index 28f1e83c56..353d8bb72f 100644
--- a/src/widgets/menu-select/__tests__/menu-select-test.js
+++ b/src/widgets/menu-select/__tests__/menu-select-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import menuSelect from '../menu-select';
describe('menuSelect', () => {
@@ -13,26 +11,55 @@ describe('menuSelect', () => {
expect(menuSelect.bind(null, { attributeName })).toThrow(/^Usage/);
});
- it('render correctly', () => {
- const data = { data: [{ name: 'foo' }, { name: 'bar' }] };
- const results = { getFacetValues: sinon.spy(() => data) };
- const state = { toggleRefinement: sinon.spy() };
- const helper = {
- toggleRefinement: sinon.stub().returnsThis(),
- search: sinon.spy(),
- state,
- };
- const createURL = () => '#';
- const ReactDOM = { render: sinon.spy() };
- menuSelect.__Rewire__('render', ReactDOM.render);
- const widget = menuSelect({
- container: document.createElement('div'),
- attributeName: 'test',
+ describe('render', () => {
+ let ReactDOM;
+ let data;
+ let results;
+ let state;
+ let helper;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ menuSelect.__Rewire__('render', ReactDOM.render);
+
+ data = { data: [{ name: 'foo' }, { name: 'bar' }] };
+ results = { getFacetValues: jest.fn(() => data) };
+ state = { toggleRefinement: jest.fn() };
+ helper = {
+ toggleRefinement: jest.fn().mockReturnThis(),
+ search: jest.fn(),
+ state,
+ };
+ });
+
+ it('renders correctly', () => {
+ const widget = menuSelect({
+ container: document.createElement('div'),
+ attributeName: 'test',
+ });
+
+ widget.init({ helper, createURL: () => '#', instantSearchInstance: {} });
+ widget.render({ results, createURL: () => '#', state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ it('renders transformed items correctly', () => {
+ const widget = menuSelect({
+ container: document.createElement('div'),
+ attributeName: 'test',
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, createURL: () => '#', instantSearchInstance: {} });
+ widget.render({ results, createURL: () => '#', state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ afterEach(() => {
+ menuSelect.__ResetDependency__('render');
});
- const instantSearchInstance = { templatesConfig: undefined };
- widget.init({ helper, createURL, instantSearchInstance });
- widget.render({ results, createURL, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- menuSelect.__ResetDependency__('render');
});
});
diff --git a/src/widgets/menu-select/menu-select.js b/src/widgets/menu-select/menu-select.js
index d8c7323743..3347320f83 100644
--- a/src/widgets/menu-select/menu-select.js
+++ b/src/widgets/menu-select/menu-select.js
@@ -59,6 +59,7 @@ menuSelect({
[ templates.{header,item,footer,seeAllOption} ],
[ transformData.{item} ],
[ autoHideContainer ]
+ [ transformItems ]
})`;
/**
@@ -95,6 +96,7 @@ menuSelect({
* @property {MenuSelectTransforms} [transformData] Set of functions to update the data before passing them to the templates.
* @property {boolean} [autoHideContainer=true] Hide the container when there are no items in the menu select.
* @property {MenuSelectCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -124,6 +126,7 @@ export default function menuSelect({
templates = defaultTemplates,
transformData,
autoHideContainer = true,
+ transformItems,
}) {
if (!container || !attributeName) {
throw new Error(usage);
@@ -149,7 +152,7 @@ export default function menuSelect({
try {
const makeWidget = connectMenu(specializedRenderer);
- return makeWidget({ attributeName, limit, sortBy });
+ return makeWidget({ attributeName, limit, sortBy, transformItems });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
index be6e9bce33..20c39b0f6e 100644
--- a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
+++ b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
@@ -1,6 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`menu snapshot 1`] = `
+exports[`menu render renders transformed items 1`] = `
+{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}",
+ },
+ "templatesConfig": undefined,
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "footer": false,
+ "header": false,
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
+/>
+`;
+
+exports[`menu render snapshot 1`] = `
{
it('throws an exception when no attributeName', () => {
const container = document.createElement('div');
@@ -13,26 +11,63 @@ describe('menu', () => {
expect(menu.bind(null, { attributeName })).toThrow(/^Usage/);
});
- it('snapshot', () => {
- const data = { data: [{ name: 'foo' }, { name: 'bar' }] };
- const results = { getFacetValues: sinon.spy(() => data) };
- const state = { toggleRefinement: sinon.spy() };
- const helper = {
- toggleRefinement: sinon.stub().returnsThis(),
- search: sinon.spy(),
- state,
- };
- const createURL = () => '#';
- const ReactDOM = { render: sinon.spy() };
- menu.__Rewire__('render', ReactDOM.render);
- const widget = menu({
- container: document.createElement('div'),
- attributeName: 'test',
+ describe('render', () => {
+ let ReactDOM;
+ let data;
+ let results;
+ let state;
+ let helper;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ menu.__Rewire__('render', ReactDOM.render);
+
+ data = { data: [{ name: 'foo' }, { name: 'bar' }] };
+ results = { getFacetValues: jest.fn(() => data) };
+ state = { toggleRefinement: jest.fn() };
+ helper = {
+ toggleRefinement: jest.fn().mockReturnThis(),
+ search: jest.fn(),
+ state,
+ };
+ });
+
+ it('snapshot', () => {
+ const widget = menu({
+ container: document.createElement('div'),
+ attributeName: 'test',
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#',
+ instantSearchInstance: { templatesConfig: undefined },
+ });
+ widget.render({ results, state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ it('renders transformed items', () => {
+ const widget = menu({
+ container: document.createElement('div'),
+ attributeName: 'test',
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#',
+ instantSearchInstance: { templatesConfig: undefined },
+ });
+ widget.render({ results, state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+
+ afterEach(() => {
+ menu.__ResetDependency__('render');
});
- const instantSearchInstance = { templatesConfig: undefined };
- widget.init({ helper, createURL, instantSearchInstance });
- widget.render({ results, createURL, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- menu.__ResetDependency__('render');
});
});
diff --git a/src/widgets/menu/menu.js b/src/widgets/menu/menu.js
index df968f903f..0f5f2cd3cd 100644
--- a/src/widgets/menu/menu.js
+++ b/src/widgets/menu/menu.js
@@ -82,7 +82,8 @@ menu({
[ transformData.{item} ],
[ autoHideContainer ],
[ showMore.{templates: {active, inactive}, limit} ],
- [ collapsible=false ]
+ [ collapsible=false ],
+ [ transformItems ]
})`;
/**
@@ -136,6 +137,7 @@ menu({
* @property {boolean} [autoHideContainer=true] Hide the container when there are no items in the menu.
* @property {MenuCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {boolean|{collapsible: boolean}} [collapsible=false] Hide the widget body and footer when clicking on header.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -174,6 +176,7 @@ export default function menu({
transformData,
autoHideContainer = true,
showMore = false,
+ transformItems,
}) {
if (!container) {
throw new Error(usage);
@@ -220,7 +223,13 @@ export default function menu({
const makeWidget = connectMenu(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeWidget({ attributeName, limit, sortBy, showMoreLimit });
+ return makeWidget({
+ attributeName,
+ limit,
+ sortBy,
+ showMoreLimit,
+ transformItems,
+ });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap b/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap
index 5cd3109531..ac3a73b639 100644
--- a/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap
+++ b/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap
@@ -137,3 +137,77 @@ exports[`numericRefinementList() calls twice ReactDOM.render(
`;
+
+exports[`numericRefinementList() renders with transformed items 1`] = `
+
+ {{label}}
+",
+ },
+ "templatesConfig": undefined,
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "footer": false,
+ "header": false,
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+/>
+`;
diff --git a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js b/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js
index 1f6c98a0c9..df46b6728c 100644
--- a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js
+++ b/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js
@@ -1,6 +1,3 @@
-import expect from 'expect';
-import sinon from 'sinon';
-import cloneDeep from 'lodash/cloneDeep';
import numericRefinementList from '../numeric-refinement-list.js';
const encodeValue = (start, end) =>
@@ -44,7 +41,7 @@ describe('numericRefinementList()', () => {
let state;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
numericRefinementList.__Rewire__('render', ReactDOM.render);
options = [
@@ -64,23 +61,23 @@ describe('numericRefinementList()', () => {
});
helper = {
state: {
- getNumericRefinements: sinon.stub().returns([]),
+ getNumericRefinements: jest.fn().mockReturnValue([]),
},
- addNumericRefinement: sinon.spy(),
- search: sinon.spy(),
- setState: sinon.stub().returnsThis(),
+ addNumericRefinement: jest.fn(),
+ search: jest.fn(),
+ setState: jest.fn().mockReturnThis(),
};
state = {
- getNumericRefinements: sinon.stub().returns([]),
- clearRefinements: sinon.stub().returnsThis(),
- addNumericRefinement: sinon.stub().returnsThis(),
+ getNumericRefinements: jest.fn().mockReturnValue([]),
+ clearRefinements: jest.fn().mockReturnThis(),
+ addNumericRefinement: jest.fn().mockReturnThis(),
};
results = {
hits: [],
};
- helper.state.clearRefinements = sinon.stub().returns(helper.state);
- helper.state.addNumericRefinement = sinon.stub().returns(helper.state);
+ helper.state.clearRefinements = jest.fn().mockReturnValue(helper.state);
+ helper.state.addNumericRefinement = jest.fn().mockReturnValue(helper.state);
createURL = () => '#';
widget.init({ helper, instantSearchInstance: {} });
});
@@ -89,101 +86,121 @@ describe('numericRefinementList()', () => {
widget.render({ state, results, createURL });
widget.render({ state, results, createURL });
- expect(ReactDOM.render.callCount).toBe(2);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+
+ it('renders with transformed items', () => {
+ widget = numericRefinementList({
+ container,
+ attributeName: 'price',
+ options,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, instantSearchInstance: {} });
+ widget.render({ state, results, createURL });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it("doesn't call the refinement functions if not refined", () => {
widget.render({ state, results, createURL });
- expect(helper.state.clearRefinements.called).toBe(
- false,
+ expect(helper.state.clearRefinements).toHaveBeenCalledTimes(
+ 0,
'clearRefinements called one'
);
- expect(helper.state.addNumericRefinement.called).toBe(
- false,
+ expect(helper.state.addNumericRefinement).toHaveBeenCalledTimes(
+ 0,
'addNumericRefinement never called'
);
- expect(helper.search.called).toBe(false, 'search never called');
+ expect(helper.search).toHaveBeenCalledTimes(0, 'search never called');
});
it('calls the refinement functions if refined with "4"', () => {
widget._refine(encodeValue(4, 4));
- expect(helper.state.clearRefinements.calledOnce).toBe(
- true,
+ expect(helper.state.clearRefinements).toHaveBeenCalledTimes(
+ 1,
'clearRefinements called once'
);
- expect(helper.state.addNumericRefinement.calledOnce).toBe(
- true,
+ expect(helper.state.addNumericRefinement).toHaveBeenCalledTimes(
+ 1,
'addNumericRefinement called once'
);
- expect(helper.state.addNumericRefinement.getCall(0).args).toEqual([
+ expect(helper.state.addNumericRefinement).toHaveBeenNthCalledWith(
+ 1,
'price',
'=',
- 4,
- ]);
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ 4
+ );
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('calls the refinement functions if refined with "between 5 and 10"', () => {
widget._refine(encodeValue(5, 10));
- expect(helper.state.clearRefinements.calledOnce).toBe(
- true,
+ expect(helper.state.clearRefinements).toHaveBeenCalledTimes(
+ 1,
'clearRefinements called once'
);
- expect(helper.state.addNumericRefinement.calledTwice).toBe(
- true,
+ expect(helper.state.addNumericRefinement).toHaveBeenCalledTimes(
+ 2,
'addNumericRefinement called twice'
);
- expect(helper.state.addNumericRefinement.getCall(0).args).toEqual([
+ expect(helper.state.addNumericRefinement).toHaveBeenNthCalledWith(
+ 1,
'price',
'>=',
- 5,
- ]);
- expect(helper.state.addNumericRefinement.getCall(1).args).toEqual([
+ 5
+ );
+ expect(helper.state.addNumericRefinement).toHaveBeenNthCalledWith(
+ 2,
'price',
'<=',
- 10,
- ]);
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ 10
+ );
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('calls two times the refinement functions if refined with "less than 4"', () => {
widget._refine(encodeValue(undefined, 4));
- expect(helper.state.clearRefinements.calledOnce).toBe(
- true,
+ expect(helper.state.clearRefinements).toHaveBeenCalledTimes(
+ 1,
'clearRefinements called once'
);
- expect(helper.state.addNumericRefinement.calledOnce).toBe(
- true,
+ expect(helper.state.addNumericRefinement).toHaveBeenCalledTimes(
+ 1,
'addNumericRefinement called once'
);
- expect(helper.state.addNumericRefinement.getCall(0).args).toEqual([
+ expect(helper.state.addNumericRefinement).toHaveBeenNthCalledWith(
+ 1,
'price',
'<=',
- 4,
- ]);
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ 4
+ );
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('calls two times the refinement functions if refined with "more than 10"', () => {
widget._refine(encodeValue(10));
- expect(helper.state.clearRefinements.calledOnce).toBe(
- true,
+ expect(helper.state.clearRefinements).toHaveBeenCalledTimes(
+ 1,
'clearRefinements called once'
);
- expect(helper.state.addNumericRefinement.calledOnce).toBe(
- true,
+ expect(helper.state.addNumericRefinement).toHaveBeenCalledTimes(
+ 1,
'addNumericRefinement called once'
);
- expect(helper.state.addNumericRefinement.getCall(0).args).toEqual([
+ expect(helper.state.addNumericRefinement).toHaveBeenNthCalledWith(
+ 1,
'price',
'>=',
- 10,
- ]);
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ 10
+ );
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('does not alter the initial options when rendering', () => {
@@ -193,7 +210,7 @@ describe('numericRefinementList()', () => {
// Given
const initialOptions = [{ start: 0, end: 5, name: '1-5' }];
- const initialOptionsClone = cloneDeep(initialOptions);
+ const initialOptionsClone = [...initialOptions];
const testWidget = numericRefinementList({
container,
attributeName: 'price',
diff --git a/src/widgets/numeric-refinement-list/numeric-refinement-list.js b/src/widgets/numeric-refinement-list/numeric-refinement-list.js
index 356e97c220..3d90944ab1 100644
--- a/src/widgets/numeric-refinement-list/numeric-refinement-list.js
+++ b/src/widgets/numeric-refinement-list/numeric-refinement-list.js
@@ -58,7 +58,8 @@ numericRefinementList({
[ templates.{header,item,footer} ],
[ transformData.{item} ],
[ autoHideContainer ],
- [ collapsible=false ]
+ [ collapsible=false ],
+ [ transformItems ]
})`;
/**
@@ -103,6 +104,7 @@ numericRefinementList({
* @property {boolean} [autoHideContainer=true] Hide the container when no results match.
* @property {NumericRefinementListCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {boolean|{collapsible: boolean}} [collapsible=false] Hide the widget body and footer when clicking on header.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -146,6 +148,7 @@ export default function numericRefinementList({
collapsible = false,
transformData,
autoHideContainer = true,
+ transformItems,
} = {}) {
if (!container || !attributeName || !options) {
throw new Error(usage);
@@ -179,7 +182,11 @@ export default function numericRefinementList({
specializedRenderer,
() => unmountComponentAtNode(containerNode)
);
- return makeNumericRefinementList({ attributeName, options });
+ return makeNumericRefinementList({
+ attributeName,
+ options,
+ transformItems,
+ });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/numeric-selector/numeric-selector.js b/src/widgets/numeric-selector/numeric-selector.js
index 92677c5351..7c7d676b72 100644
--- a/src/widgets/numeric-selector/numeric-selector.js
+++ b/src/widgets/numeric-selector/numeric-selector.js
@@ -31,7 +31,8 @@ const usage = `Usage: numericSelector({
attributeName,
options,
cssClasses.{root,select,item},
- autoHideContainer
+ autoHideContainer,
+ transformItems
})`;
/**
@@ -55,6 +56,7 @@ const usage = `Usage: numericSelector({
* @property {NumericOption[]} options Array of objects defining the different values and labels.
* @property {string} [operator='='] The operator to use to refine.
* @property {boolean} [autoHideContainer=false] Hide the container when no results match.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
* @property {NumericSelectorCSSClasses} [cssClasses] CSS classes to be added.
*/
@@ -96,6 +98,7 @@ export default function numericSelector({
options,
cssClasses: userCssClasses = {},
autoHideContainer = false,
+ transformItems,
}) {
const containerNode = getContainerNode(container);
if (!container || !options || options.length === 0 || !attributeName) {
@@ -121,7 +124,12 @@ export default function numericSelector({
specializedRenderer,
() => unmountComponentAtNode(containerNode)
);
- return makeNumericSelector({ operator, attributeName, options });
+ return makeNumericSelector({
+ operator,
+ attributeName,
+ options,
+ transformItems,
+ });
} catch (e) {
throw new Error(usage);
}
diff --git a/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap b/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap
new file mode 100644
index 0000000000..a25fdd6910
--- /dev/null
+++ b/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap
@@ -0,0 +1,78 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`refinementList() render renders transformed items correctly 1`] = `
+
+
+ {{{highlighted}}}
+ {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
+",
+ },
+ "templatesConfig": Object {},
+ "transformData": undefined,
+ "useCustomCompileOptions": Object {
+ "footer": false,
+ "header": false,
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
+/>
+`;
diff --git a/src/widgets/refinement-list/__tests__/refinement-list-test.js b/src/widgets/refinement-list/__tests__/refinement-list-test.js
index fa8e8e06b2..ec1bb97b68 100644
--- a/src/widgets/refinement-list/__tests__/refinement-list-test.js
+++ b/src/widgets/refinement-list/__tests__/refinement-list-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import algoliasearchHelper from 'algoliasearch-helper';
const SearchParameters = algoliasearchHelper.SearchParameters;
import refinementList from '../refinement-list.js';
@@ -16,11 +14,11 @@ describe('refinementList()', () => {
beforeEach(() => {
container = document.createElement('div');
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
refinementList.__Rewire__('render', ReactDOM.render);
- autoHideContainer = sinon.stub().returnsArg(0);
+ autoHideContainer = jest.fn();
refinementList.__Rewire__('autoHideContainerHOC', autoHideContainer);
- headerFooter = sinon.stub().returnsArg(0);
+ headerFooter = jest.fn();
refinementList.__Rewire__('headerFooterHOC', headerFooter);
});
@@ -52,9 +50,9 @@ describe('refinementList()', () => {
beforeEach(() => {
options = { container, attributeName: 'attributeName' };
results = {
- getFacetValues: sinon
- .stub()
- .returns([{ name: 'foo' }, { name: 'bar' }]),
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([{ name: 'foo' }, { name: 'bar' }]),
};
state = SearchParameters.make({});
createURL = () => '#';
@@ -78,7 +76,7 @@ describe('refinementList()', () => {
// When
renderWidget({ cssClasses });
- const actual = ReactDOM.render.firstCall.args[0].props.cssClasses;
+ const actual = ReactDOM.render.mock.calls[0][0].props.cssClasses;
// Then
expect(actual.root).toBe('ais-refinement-list root cx');
@@ -97,26 +95,26 @@ describe('refinementList()', () => {
describe('autoHideContainer', () => {
it('should set shouldAutoHideContainer to false if there are facetValues', () => {
// Given
- results.getFacetValues = sinon
- .stub()
- .returns([{ name: 'foo' }, { name: 'bar' }]);
+ results.getFacetValues = jest
+ .fn()
+ .mockReturnValue([{ name: 'foo' }, { name: 'bar' }]);
// When
renderWidget();
const actual =
- ReactDOM.render.firstCall.args[0].props.shouldAutoHideContainer;
+ ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer;
// Then
expect(actual).toBe(false);
});
it('should set shouldAutoHideContainer to true if no facet values', () => {
// Given
- results.getFacetValues = sinon.stub().returns([]);
+ results.getFacetValues = jest.fn().mockReturnValue([]);
// When
renderWidget();
const actual =
- ReactDOM.render.firstCall.args[0].props.shouldAutoHideContainer;
+ ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer;
// Then
expect(actual).toBe(true);
@@ -140,11 +138,11 @@ describe('refinementList()', () => {
isRefined: false,
},
];
- results.getFacetValues = sinon.stub().returns(facetValues);
+ results.getFacetValues = jest.fn().mockReturnValue(facetValues);
// When
renderWidget();
- const props = ReactDOM.render.firstCall.args[0].props;
+ const props = ReactDOM.render.mock.calls[0][0].props;
// Then
expect(props.headerFooterData.header.refinedFacetsCount).toEqual(2);
@@ -164,7 +162,7 @@ describe('refinementList()', () => {
isRefined: false,
},
];
- results.getFacetValues = sinon.stub().returns(facetValues);
+ results.getFacetValues = jest.fn().mockReturnValue(facetValues);
const renderOptions = { results, helper, state };
// When
@@ -173,7 +171,7 @@ describe('refinementList()', () => {
widget.render(renderOptions);
// Then
- let props = ReactDOM.render.firstCall.args[0].props;
+ let props = ReactDOM.render.mock.calls[0][0].props;
expect(props.headerFooterData.header.refinedFacetsCount).toEqual(1);
// When... second render call
@@ -181,10 +179,27 @@ describe('refinementList()', () => {
widget.render(renderOptions);
// Then
- props = ReactDOM.render.secondCall.args[0].props;
+ props = ReactDOM.render.mock.calls[1][0].props;
expect(props.headerFooterData.header.refinedFacetsCount).toEqual(2);
});
});
+
+ it('renders transformed items correctly', () => {
+ widget = refinementList({
+ ...options,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({
+ helper,
+ createURL,
+ instantSearchInstance,
+ });
+ widget.render({ results, helper, state });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
});
describe('show more', () => {
diff --git a/src/widgets/refinement-list/refinement-list.js b/src/widgets/refinement-list/refinement-list.js
index 76cb1ddd28..bfe0e73834 100644
--- a/src/widgets/refinement-list/refinement-list.js
+++ b/src/widgets/refinement-list/refinement-list.js
@@ -99,6 +99,7 @@ refinementList({
[ showMore.{templates: {active, inactive}, limit} ],
[ collapsible=false ],
[ searchForFacetValues.{placeholder, templates: {noResults}, isAlwaysActive, escapeFacetValues}],
+ [ transformItems ],
})`;
/**
@@ -176,6 +177,7 @@ refinementList({
* @property {string} attributeName Name of the attribute for faceting.
* @property {"and"|"or"} [operator="or"] How to apply refinements. Possible values: `or`, `and`
* @property {string[]|function} [sortBy=["isRefined", "count:desc", "name:asc"]] How to sort refinements. Possible values: `count:asc` `count:desc` `name:asc` `name:desc` `isRefined`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
* @property {number} [limit=10] How much facet values to get. When the show more feature is activated this is the minimum number of facets requested (the show more button is not in active state).
@@ -238,6 +240,7 @@ export default function refinementList({
autoHideContainer = true,
showMore = false,
searchForFacetValues = false,
+ transformItems,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -301,6 +304,7 @@ export default function refinementList({
showMoreLimit,
sortBy,
escapeFacetValues,
+ transformItems,
});
} catch (e) {
throw new Error(usage);
diff --git a/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap b/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap
index 7d580870c5..3aeb8f1b57 100644
--- a/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap
+++ b/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap
@@ -53,3 +53,32 @@ exports[`sortBySelector() calls twice ReactDOM.render(, contai
shouldAutoHideContainer={false}
/>
`;
+
+exports[`sortBySelector() renders transformed items 1`] = `
+
+`;
diff --git a/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js b/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js
index f88ded71bb..4fa34113ad 100644
--- a/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js
+++ b/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-import expect from 'expect';
import sortBySelector from '../sort-by-selector';
import Selector from '../../../components/Selector';
@@ -34,8 +32,8 @@ describe('sortBySelector()', () => {
indexName: 'defaultIndex',
createAlgoliaClient: () => ({}),
});
- autoHideContainer = sinon.stub().returns(Selector);
- ReactDOM = { render: sinon.spy() };
+ autoHideContainer = jest.fn().mockReturnValue(Selector);
+ ReactDOM = { render: jest.fn() };
sortBySelector.__Rewire__('render', ReactDOM.render);
sortBySelector.__Rewire__('autoHideContainerHOC', autoHideContainer);
@@ -52,9 +50,9 @@ describe('sortBySelector()', () => {
};
widget = sortBySelector({ container, indices, cssClasses });
helper = {
- getIndex: sinon.stub().returns('index-a'),
- setIndex: sinon.stub().returnsThis(),
- search: sinon.spy(),
+ getIndex: jest.fn().mockReturnValue('index-a'),
+ setIndex: jest.fn().mockReturnThis(),
+ search: jest.fn(),
};
results = {
@@ -71,20 +69,34 @@ describe('sortBySelector()', () => {
it('calls twice ReactDOM.render(, container)', () => {
widget.render({ helper, results });
widget.render({ helper, results });
- expect(ReactDOM.render.calledTwice).toBe(
- true,
+ expect(ReactDOM.render).toHaveBeenCalledTimes(
+ 2,
'ReactDOM.render called twice'
);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+
+ it('renders transformed items', () => {
+ widget = sortBySelector({
+ container,
+ indices,
+ transformItems: items =>
+ items.map(item => ({ ...item, transformed: true })),
+ });
+
+ widget.init({ helper, instantSearchInstance: {} });
+ widget.render({ helper, results });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('sets the underlying index', () => {
widget.setIndex('index-b');
- expect(helper.setIndex.calledOnce).toBe(true, 'setIndex called once');
- expect(helper.search.calledOnce).toBe(true, 'search called once');
+ expect(helper.setIndex).toHaveBeenCalledTimes(1, 'setIndex called once');
+ expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
it('should throw if there is no name attribute in a passed object', () => {
@@ -96,7 +108,7 @@ describe('sortBySelector()', () => {
});
it('must include the current index at initialization time', () => {
- helper.getIndex = sinon.stub().returns('non-existing-index');
+ helper.getIndex = jest.fn().mockReturnValue('non-existing-index');
expect(() => {
widget.init({ helper });
}).toThrow(/Index non-existing-index not present/);
diff --git a/src/widgets/sort-by-selector/sort-by-selector.js b/src/widgets/sort-by-selector/sort-by-selector.js
index 458ce6ce43..40882f08cf 100644
--- a/src/widgets/sort-by-selector/sort-by-selector.js
+++ b/src/widgets/sort-by-selector/sort-by-selector.js
@@ -32,7 +32,8 @@ sortBySelector({
container,
indices,
[cssClasses.{root,select,item}={}],
- [autoHideContainer=false]
+ [autoHideContainer=false],
+ [transformItems]
})`;
/**
@@ -54,6 +55,7 @@ sortBySelector({
* @property {SortByIndexDefinition[]} indices Array of objects defining the different indices to choose from.
* @property {boolean} [autoHideContainer=false] Hide the container when no results match.
* @property {SortByWidgetCssClasses} [cssClasses] CSS classes to be added.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -83,6 +85,7 @@ export default function sortBySelector({
indices,
cssClasses: userCssClasses = {},
autoHideContainer = false,
+ transformItems,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -108,7 +111,7 @@ export default function sortBySelector({
const makeWidget = connectSortBySelector(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeWidget({ indices });
+ return makeWidget({ indices, transformItems });
} catch (e) {
throw new Error(usage);
}