From 3bbe75e695ee5104a9153181e7569ed450dab5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Mon, 12 Aug 2019 15:47:02 +0200 Subject: [PATCH 1/4] feat(clearRefinements): support multiple indices --- .../__tests__/connectClearRefinements-test.js | 467 +++++++++++------- .../connectClearRefinements.js | 143 +++--- .../clear-refinements-test.js.snap | 16 - .../__tests__/clear-refinements-test.js | 141 +++--- stories/clear-refinements.stories.js | 50 ++ test/mock/createWidget.ts | 6 +- 6 files changed, 465 insertions(+), 358 deletions(-) diff --git a/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js index a20580fe8f..25f2f11176 100644 --- a/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js +++ b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js @@ -1,4 +1,8 @@ import jsHelper, { SearchResults } from 'algoliasearch-helper'; +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/mock/createWidget'; import connectClearRefinements from '../connectClearRefinements'; describe('connectClearRefinements', () => { @@ -48,10 +52,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin describe('Lifecycle', () => { it('renders during init and render', () => { - const helper = jsHelper({}); + const helper = jsHelper({}, 'indexName'); helper.search = () => {}; - // test that the dummyRendering is called with the isFirstRendering - // flag set accordingly const rendering = jest.fn(); const makeWidget = connectClearRefinements(rendering); const widget = makeWidget({ @@ -59,46 +61,59 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin }); expect(widget.getConfiguration).toBe(undefined); - // test if widget is not rendered yet at this point expect(rendering).toHaveBeenCalledTimes(0); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); // test that rendering has been called during init with isFirstRendering = true expect(rendering).toHaveBeenCalledTimes(1); - // test if isFirstRendering is true during init - expect(rendering.mock.calls[0][1]).toBe(true); - const firstRenderingOptions = rendering.mock.calls[0][0]; + const [ + firstRenderingOptions, + isFirstRenderAtInit, + ] = rendering.mock.calls[0]; + + expect(isFirstRenderAtInit).toBe(true); + expect(firstRenderingOptions.createURL).toBeInstanceOf(Function); + expect(firstRenderingOptions.refine).toBeInstanceOf(Function); expect(firstRenderingOptions.hasRefinements).toBe(false); expect(firstRenderingOptions.widgetParams).toEqual({ foo: 'bar', // dummy param to test `widgetParams` }); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); // test that rendering has been called during init with isFirstRendering = false expect(rendering).toHaveBeenCalledTimes(2); - expect(rendering.mock.calls[1][1]).toBe(false); - const secondRenderingOptions = rendering.mock.calls[1][0]; + const [ + secondRenderingOptions, + isFirstRenderAtRender, + ] = rendering.mock.calls[1]; + + expect(isFirstRenderAtRender).toBe(false); + expect(secondRenderingOptions.createURL).toBeInstanceOf(Function); + expect(secondRenderingOptions.refine).toBeInstanceOf(Function); expect(secondRenderingOptions.hasRefinements).toBe(false); }); it('does not throw without the unmount function', () => { - const helper = jsHelper({}); + const helper = jsHelper({}, 'indexName'); const rendering = () => {}; const makeWidget = connectClearRefinements(rendering); const widget = makeWidget(); + expect(() => widget.dispose({ helper, state: helper.state }) ).not.toThrow(); @@ -107,7 +122,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin describe('Instance options', () => { it('provides a function to clear the refinements', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['myFacet'], }); helper.search = () => {}; @@ -118,39 +133,37 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin const makeWidget = connectClearRefinements(rendering); const widget = makeWidget({}); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); - - expect(helper.hasRefinements('myFacet')).toBe(true); - expect(helper.state.query).toBe('not empty'); - const initClearMethod = rendering.mock.calls[0][0].refine; - initClearMethod(); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); - expect(helper.hasRefinements('myFacet')).toBe(false); - expect(helper.state.query).toBe('not empty'); + expect(rendering.mock.calls[0][0].refine).toBeInstanceOf(Function); helper.toggleRefinement('myFacet', 'someOtherValue'); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('myFacet')).toBe(true); expect(helper.state.query).toBe('not empty'); - const renderClearMethod = rendering.mock.calls[1][0].refine; - renderClearMethod(); + + const refine = rendering.mock.calls[1][0].refine; + refine(); + expect(helper.hasRefinements('myFacet')).toBe(false); expect(helper.state.query).toBe('not empty'); }); it('provides a function to clear the refinements and the query', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['myFacet'], }); helper.search = () => {}; @@ -161,34 +174,32 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin const makeWidget = connectClearRefinements(rendering); const widget = makeWidget({ excludedAttributes: [] }); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); - - expect(helper.hasRefinements('myFacet')).toBe(true); - expect(helper.state.query).toBe('a query'); - const initClearMethod = rendering.mock.calls[0][0].refine; - initClearMethod(); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); - expect(helper.hasRefinements('myFacet')).toBe(false); - expect(helper.state.query).toBe(''); + expect(rendering.mock.calls[0][0].refine).toBeInstanceOf(Function); helper.toggleRefinement('myFacet', 'someOtherValue'); helper.setQuery('another query'); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('myFacet')).toBe(true); expect(helper.state.query).toBe('another query'); - const renderClearMethod = rendering.mock.calls[1][0].refine; - renderClearMethod(); + + const refine = rendering.mock.calls[1][0].refine; + refine(); + expect(helper.hasRefinements('myFacet')).toBe(false); expect(helper.state.query).toBe(''); }); @@ -204,20 +215,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin const makeWidget = connectClearRefinements(rendering); const widget = makeWidget(); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); - expect(rendering.mock.calls[0][0].hasRefinements).toBe(true); + expect(rendering.mock.calls[0][0].hasRefinements).toBe(false); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[1][0].hasRefinements).toBe(true); }); @@ -237,20 +250,22 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin excludedAttributes: [], }); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); - expect(rendering.mock.calls[0][0].hasRefinements).toBe(true); + expect(rendering.mock.calls[0][0].hasRefinements).toBe(false); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[1][0].hasRefinements).toBe(true); }); @@ -267,26 +282,28 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin excludedAttributes: [], }); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[0][0].hasRefinements).toBe(false); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[1][0].hasRefinements).toBe(false); }); it('without includedAttributes or excludedAttributes and with a query has no refinements', () => { - const helper = jsHelper({}); + const helper = jsHelper({}, 'indexName'); helper.setQuery('not empty'); helper.search = () => {}; @@ -294,26 +311,28 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin const makeWidget = connectClearRefinements(rendering); const widget = makeWidget({}); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[0][0].hasRefinements).toBe(false); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(rendering.mock.calls[1][0].hasRefinements).toBe(false); }); it('includes only includedAttributes', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['facet1', 'facet2'], }); helper.search = () => {}; @@ -327,30 +346,42 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin .toggleRefinement('facet2', 'value') .setQuery('not empty'); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(true); expect(helper.hasRefinements('facet2')).toBe(true); - const refine = rendering.mock.calls[0][0].refine; + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const refine = rendering.mock.calls[1][0].refine; refine(); - widget.render({ - helper, - state: helper.state, - createURL: () => '#', - }); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(false); expect(helper.hasRefinements('facet2')).toBe(true); - expect(rendering.mock.calls[1][0].hasRefinements).toBe(false); + expect(rendering.mock.calls[2][0].hasRefinements).toBe(false); }); it('includes only includedAttributes (with query)', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['facet1'], }); helper.search = () => {}; @@ -361,30 +392,42 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin helper.toggleRefinement('facet1', 'value').setQuery('not empty'); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(true); expect(helper.state.query).toBe('not empty'); - const refine = rendering.mock.calls[0][0].refine; + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const refine = rendering.mock.calls[1][0].refine; refine(); - widget.render({ - helper, - state: helper.state, - createURL: () => '#', - }); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(false); expect(helper.state.query).toBe(''); - expect(rendering.mock.calls[1][0].hasRefinements).toBe(false); + expect(rendering.mock.calls[2][0].hasRefinements).toBe(false); }); it('excludes excludedAttributes', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['facet1', 'facet2'], }); helper.search = () => {}; @@ -402,48 +445,66 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin { helper.setQuery('not empty'); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(true); expect(helper.hasRefinements('facet2')).toBe(true); - const refine = rendering.mock.calls[0][0].refine; + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const refine = rendering.mock.calls[1][0].refine; refine(); expect(helper.hasRefinements('facet1')).toBe(false); expect(helper.hasRefinements('facet2')).toBe(true); - expect(rendering.mock.calls[0][0].hasRefinements).toBe(true); + expect(rendering.mock.calls[1][0].hasRefinements).toBe(true); } { // facet has not been cleared and it is still refined with value helper.setQuery('not empty'); - widget.render({ - helper, - state: helper.state, - results: new SearchResults(helper.state, [{}]), - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(false); expect(helper.hasRefinements('facet2')).toBe(true); - const refine = rendering.mock.calls[1][0].refine; + const refine = rendering.mock.calls[2][0].refine; refine(); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + expect(helper.hasRefinements('facet1')).toBe(false); expect(helper.hasRefinements('facet2')).toBe(true); } }); - describe('transformItems is called', () => { - const helper = jsHelper({}, '', { + it('transformItems is called', () => { + const helper = jsHelper({}, 'indexName', { facets: ['facet1', 'facet2', 'facet3'], }); helper.search = () => {}; @@ -464,35 +525,47 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin .toggleRefinement('facet3', 'value') .setQuery('not empty'); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(true); expect(helper.hasRefinements('facet2')).toBe(true); expect(helper.hasRefinements('facet3')).toBe(true); expect(helper.state.query).toBe('not empty'); - const refine = rendering.mock.calls[0][0].refine; + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const refine = rendering.mock.calls[1][0].refine; refine(); - widget.render({ - helper, - state: helper.state, - createURL: () => '#', - }); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); expect(helper.hasRefinements('facet1')).toBe(true); expect(helper.hasRefinements('facet2')).toBe(true); expect(helper.hasRefinements('facet3')).toBe(false); expect(helper.state.query).toBe(''); - expect(rendering.mock.calls[1][0].hasRefinements).toBe(false); + expect(rendering.mock.calls[2][0].hasRefinements).toBe(false); }); describe('createURL', () => { it('consistent with the list of excludedAttributes', () => { - const helper = jsHelper({}, '', { + const helper = jsHelper({}, 'indexName', { facets: ['facet', 'otherFacet'], }); helper.search = () => {}; @@ -509,13 +582,23 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin { helper.setQuery('not empty'); - widget.init({ - helper, - state: helper.state, - createURL: opts => opts, - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + createURL: state => state, + }) + ); - const { createURL, refine } = rendering.mock.calls[0][0]; + const { createURL, refine } = rendering.mock.calls[1][0]; // The state represented by the URL should be equal to a state // after refining. @@ -527,12 +610,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin } { - widget.render({ - helper, - state: helper.state, - results: new SearchResults(helper.state, [{}]), - createURL: () => '#', - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); const { createURL, refine } = rendering.mock.calls[1][0]; @@ -546,7 +630,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin }); it('reset the page to 0', () => { - const helper = jsHelper({}, '', {}); + const helper = jsHelper({}, 'indexName', {}); helper.search = () => {}; helper.setQuery('not empty'); @@ -554,15 +638,26 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin const makeWidget = connectClearRefinements(rendering); const widget = makeWidget({}); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); - const clearRefinements = rendering.mock.calls[0][0].refine; + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const refine = rendering.mock.calls[1][0].refine; helper.setPage(2); - clearRefinements(); + refine(); + expect(helper.state.page).toBe(0); }); }); diff --git a/src/connectors/clear-refinements/connectClearRefinements.js b/src/connectors/clear-refinements/connectClearRefinements.js index c0ac65e925..63cf8af498 100644 --- a/src/connectors/clear-refinements/connectClearRefinements.js +++ b/src/connectors/clear-refinements/connectClearRefinements.js @@ -4,6 +4,8 @@ import { getRefinements, createDocumentationMessageGenerator, noop, + uniq, + mergeSearchParameters, } from '../../lib/utils'; const withUsage = createDocumentationMessageGenerator({ @@ -89,49 +91,12 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { return { $$type: 'ais.clearRefinements', - init({ helper, instantSearchInstance, createURL }) { - const attributesToClear = getAttributesToClear({ - helper, - includedAttributes, - excludedAttributes, - transformItems, - }); - const hasRefinements = attributesToClear.length > 0; - - this._refine = () => { - helper - .setState( - clearRefinements({ - helper, - attributesToClear: getAttributesToClear({ - helper, - includedAttributes, - excludedAttributes, - transformItems, - }), - }) - ) - .search(); - }; - - this._createURL = () => - createURL( - clearRefinements({ - helper, - attributesToClear: getAttributesToClear({ - helper, - includedAttributes, - excludedAttributes, - transformItems, - }), - }) - ); - + init({ instantSearchInstance }) { renderFn( { - hasRefinements, - refine: this._refine, - createURL: this._createURL, + hasRefinements: false, + refine: noop, + createURL: noop, instantSearchInstance, widgetParams, }, @@ -139,20 +104,49 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { ); }, - render({ helper, instantSearchInstance }) { - const attributesToClear = getAttributesToClear({ - helper, - includedAttributes, - excludedAttributes, - transformItems, - }); - const hasRefinements = attributesToClear.length > 0; + render({ scopedResults, createURL, instantSearchInstance }) { + const attributesToClear = scopedResults.reduce( + (results, scopedResult) => { + return results.concat( + getAttributesToClear({ + scopedResult, + includedAttributes, + excludedAttributes, + transformItems, + }) + ); + }, + [] + ); renderFn( { - hasRefinements, - refine: this._refine, - createURL: this._createURL, + hasRefinements: attributesToClear.some( + attributeToClear => attributeToClear.items.length > 0 + ), + refine: () => { + attributesToClear.forEach(({ helper: indexHelper, items }) => { + indexHelper + .setState( + clearRefinements({ + helper: indexHelper, + attributesToClear: items, + }) + ) + .search(); + }); + }, + createURL: () => + createURL( + mergeSearchParameters( + ...attributesToClear.map(({ helper: indexHelper, items }) => { + return clearRefinements({ + helper: indexHelper, + attributesToClear: items, + }); + }) + ) + ), instantSearchInstance, widgetParams, }, @@ -168,7 +162,7 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { } function getAttributesToClear({ - helper, + scopedResult, includedAttributes, excludedAttributes, transformItems, @@ -177,22 +171,31 @@ function getAttributesToClear({ includedAttributes.indexOf('query') !== -1 || excludedAttributes.indexOf('query') === -1; - return transformItems( - getRefinements(helper.lastResults || {}, helper.state, clearsQuery) - .map(refinement => refinement.attribute) - .filter( - attribute => - // If the array is empty (default case), we keep all the attributes - includedAttributes.length === 0 || - // Otherwise, only add the specified attributes - includedAttributes.indexOf(attribute) !== -1 - ) - .filter( - attribute => - // If the query is included, we ignore the default `excludedAttributes = ['query']` - (attribute === 'query' && clearsQuery) || - // Otherwise, ignore the excluded attributes - excludedAttributes.indexOf(attribute) === -1 + return { + helper: scopedResult.helper, + items: transformItems( + uniq( + getRefinements( + scopedResult.results, + scopedResult.helper.state, + clearsQuery + ) + .map(refinement => refinement.attribute) + .filter( + attribute => + // If the array is empty (default case), we keep all the attributes + includedAttributes.length === 0 || + // Otherwise, only add the specified attributes + includedAttributes.indexOf(attribute) !== -1 + ) + .filter( + attribute => + // If the query is included, we ignore the default `excludedAttributes = ['query']` + (attribute === 'query' && clearsQuery) || + // Otherwise, ignore the excluded attributes + excludedAttributes.indexOf(attribute) === -1 + ) ) - ); + ), + }; } diff --git a/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap b/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap index 808c6b3cd0..00778b0e4b 100644 --- a/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap +++ b/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap @@ -1,21 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`clearRefinements() cssClasses should add the default CSS classes 1`] = ` -Object { - "button": "ais-ClearRefinements-button", - "disabledButton": "ais-ClearRefinements-button--disabled", - "root": "ais-ClearRefinements", -} -`; - -exports[`clearRefinements() cssClasses should allow overriding CSS classes 1`] = ` -Object { - "button": "ais-ClearRefinements-button myButton myPrimaryButton", - "disabledButton": "ais-ClearRefinements-button--disabled disabled", - "root": "ais-ClearRefinements myRoot", -} -`; - exports[`clearRefinements() with refinements calls twice render(, container) 1`] = ` { const module = require.requireActual('preact-compat'); @@ -24,119 +28,87 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin }); describe('clearRefinements()', () => { - let container; - let results; - let client; - let helper; - let createURL; - beforeEach(() => { - createURL = jest.fn().mockReturnValue('#all-cleared'); - container = document.createElement('div'); - results = {}; - client = algoliasearch('APP_ID', 'API_KEY'); - helper = { - state: { - clearRefinements: jest.fn().mockReturnThis(), - clearTags: jest.fn().mockReturnThis(), - }, - search: jest.fn(), - }; - render.mockClear(); }); describe('without refinements', () => { - beforeEach(() => { - helper.state.facetsRefinements = {}; - }); - it('calls twice render(, container)', () => { + const helper = algoliasearchHelper(createSearchClient(), 'indexName', { + facetsRefinements: {}, + }); + const container = document.createElement('div'); const widget = clearRefinements({ container, }); - widget.init({ - helper, - createURL, - instantSearchInstance: { - templatesConfig: {}, - }, - }); - widget.render({ - results, - helper, - state: helper.state, - createURL, - instantSearchInstance: {}, - }); - widget.render({ - results, - helper, - state: helper.state, - createURL, - instantSearchInstance: {}, - }); + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ helper, state: helper.state })); + widget.render(createRenderOptions({ helper, state: helper.state })); expect(render).toHaveBeenCalledTimes(2); - expect(render.mock.calls[0][0]).toMatchSnapshot(); - expect(render.mock.calls[0][1]).toEqual(container); + const [firstRender, secondRender] = render.mock.calls; - expect(render.mock.calls[1][0]).toMatchSnapshot(); - expect(render.mock.calls[1][1]).toEqual(container); + expect(firstRender[0]).toMatchSnapshot(); + expect(firstRender[1]).toEqual(container); + + expect(secondRender[0]).toMatchSnapshot(); + expect(secondRender[1]).toEqual(container); }); }); describe('with refinements', () => { - beforeEach(() => { - helper.state.facetsRefinements = { something: ['something'] }; - }); - it('calls twice render(, container)', () => { + const helper = algoliasearchHelper(createSearchClient(), 'indexName', { + facetsRefinements: { + facet: ['value'], + }, + }); + const container = document.createElement('div'); const widget = clearRefinements({ container, }); - widget.init({ - helper, - createURL, - instantSearchInstance: { - templatesConfig: {}, - }, - }); - widget.render({ results, helper, state: helper.state, createURL }); - widget.render({ results, helper, state: helper.state, createURL }); + + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ helper, state: helper.state })); + widget.render(createRenderOptions({ helper, state: helper.state })); expect(render).toHaveBeenCalledTimes(2); - expect(render.mock.calls[0][0]).toMatchSnapshot(); - expect(render.mock.calls[0][1]).toEqual(container); + const [firstRender, secondRender] = render.mock.calls; + + expect(firstRender[0]).toMatchSnapshot(); + expect(firstRender[1]).toEqual(container); - expect(render.mock.calls[1][0]).toMatchSnapshot(); - expect(render.mock.calls[1][1]).toEqual(container); + expect(secondRender[0]).toMatchSnapshot(); + expect(secondRender[1]).toEqual(container); }); }); describe('cssClasses', () => { it('should add the default CSS classes', () => { - helper = algoliasearchHelper(client, 'index_name'); + const helper = algoliasearchHelper(createSearchClient(), 'indexName'); + const container = document.createElement('div'); const widget = clearRefinements({ container, }); - widget.init({ - helper, - createURL, - instantSearchInstance: { - templatesConfig: {}, - }, - }); + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ helper, state: helper.state })); - widget.render({ results, helper, state: helper.state, createURL }); - expect(render.mock.calls[0][0].props.cssClasses).toMatchSnapshot(); + expect(render.mock.calls[0][0].props.cssClasses).toMatchInlineSnapshot(` + Object { + "button": "ais-ClearRefinements-button", + "disabledButton": "ais-ClearRefinements-button--disabled", + "root": "ais-ClearRefinements", + } + `); }); it('should allow overriding CSS classes', () => { + const helper = algoliasearchHelper(createSearchClient(), 'indexName'); + const container = document.createElement('div'); const widget = clearRefinements({ container, cssClasses: { @@ -145,16 +117,17 @@ describe('clearRefinements()', () => { disabledButton: ['disabled'], }, }); - widget.init({ - helper, - createURL, - instantSearchInstance: { - templatesConfig: {}, - }, - }); - widget.render({ results, helper, state: helper.state, createURL }); - expect(render.mock.calls[0][0].props.cssClasses).toMatchSnapshot(); + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ helper, state: helper.state })); + + expect(render.mock.calls[0][0].props.cssClasses).toMatchInlineSnapshot(` + Object { + "button": "ais-ClearRefinements-button myButton myPrimaryButton", + "disabledButton": "ais-ClearRefinements-button--disabled disabled", + "root": "ais-ClearRefinements myRoot", + } + `); }); }); }); diff --git a/stories/clear-refinements.stories.js b/stories/clear-refinements.stories.js index f71a983e04..0de322dd05 100644 --- a/stories/clear-refinements.stories.js +++ b/stories/clear-refinements.stories.js @@ -79,4 +79,54 @@ storiesOf('ClearRefinements', module) }, } ) + ) + .add( + 'with multi indices', + withHits(({ search, container, instantsearch }) => { + const clearRefinementsContainer = document.createElement('div'); + const instantSearchPriceAscTitle = document.createElement('h3'); + instantSearchPriceAscTitle.innerHTML = + 'instant_search_price_asc'; + const instantSearchMediaTitle = document.createElement('h3'); + instantSearchMediaTitle.innerHTML = + 'instant_search_rating_asc'; + const refinementListContainer1 = document.createElement('div'); + const refinementListContainer2 = document.createElement('div'); + + container.appendChild(clearRefinementsContainer); + container.appendChild(instantSearchPriceAscTitle); + container.appendChild(refinementListContainer1); + container.appendChild(instantSearchMediaTitle); + container.appendChild(refinementListContainer2); + + search.addWidgets([ + instantsearch.widgets.clearRefinements({ + container: clearRefinementsContainer, + }), + + instantsearch.widgets + .index({ + indexName: 'instant_search_price_asc', + }) + .addWidgets([ + instantsearch.widgets.refinementList({ + container: refinementListContainer1, + attribute: 'brand', + limit: 3, + }), + ]), + + instantsearch.widgets + .index({ + indexName: 'instant_search_rating_asc', + }) + .addWidgets([ + instantsearch.widgets.refinementList({ + container: refinementListContainer2, + attribute: 'categories', + limit: 3, + }), + ]), + ]); + }) ); diff --git a/test/mock/createWidget.ts b/test/mock/createWidget.ts index 349d974977..d04ebe25bf 100644 --- a/test/mock/createWidget.ts +++ b/test/mock/createWidget.ts @@ -42,9 +42,11 @@ export const createRenderOptions = ( results, scopedResults: [ { - indexId: instantSearchInstance.helper!.state.index, + indexId: args.helper + ? args.helper.state.index + : instantSearchInstance.helper!.state.index, results, - helper: instantSearchInstance.helper!, + helper: args.helper || instantSearchInstance.helper!, }, ], searchMetadata: { From bb21726a8b155c360bcf7224731990a49ee8982e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 14 Aug 2019 11:02:10 +0200 Subject: [PATCH 2/4] fix(createWidget): use consistent helper --- test/mock/createWidget.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/mock/createWidget.ts b/test/mock/createWidget.ts index d04ebe25bf..2e51245251 100644 --- a/test/mock/createWidget.ts +++ b/test/mock/createWidget.ts @@ -29,6 +29,7 @@ export const createRenderOptions = ( ): RenderOptions => { const instantSearchInstance = createInstantSearch(); const response = createMultiSearchResponse(); + const helper = args.helper || instantSearchInstance.helper!; const results = new algolisearchHelper.SearchResults( instantSearchInstance.helper!.state, response.results @@ -37,16 +38,14 @@ export const createRenderOptions = ( return { instantSearchInstance, templatesConfig: instantSearchInstance.templatesConfig, - helper: instantSearchInstance.helper!, - state: instantSearchInstance.helper!.state, + helper, + state: helper.state, results, scopedResults: [ { - indexId: args.helper - ? args.helper.state.index - : instantSearchInstance.helper!.state.index, + indexId: helper.state.index, + helper, results, - helper: args.helper || instantSearchInstance.helper!, }, ], searchMetadata: { From 07a010772b1c3fc3c6c5e3a715dd703bb0fa5e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Tue, 27 Aug 2019 13:34:27 +0200 Subject: [PATCH 3/4] refactor(clearRefinements): pass same function instances to render --- .../connectClearRefinements.js | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/connectors/clear-refinements/connectClearRefinements.js b/src/connectors/clear-refinements/connectClearRefinements.js index 63cf8af498..c6fabdb955 100644 --- a/src/connectors/clear-refinements/connectClearRefinements.js +++ b/src/connectors/clear-refinements/connectClearRefinements.js @@ -88,6 +88,11 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { transformItems = items => items, } = widgetParams; + const connectorState = { + refine: noop, + createURL: noop, + }; + return { $$type: 'ais.clearRefinements', @@ -95,8 +100,8 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { renderFn( { hasRefinements: false, - refine: noop, - createURL: noop, + refine: connectorState.refine, + createURL: connectorState.createURL, instantSearchInstance, widgetParams, }, @@ -119,34 +124,38 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { [] ); + connectorState.refine = () => { + attributesToClear.forEach(({ helper: indexHelper, items }) => { + indexHelper + .setState( + clearRefinements({ + helper: indexHelper, + attributesToClear: items, + }) + ) + .search(); + }); + }; + + connectorState.createURL = () => + createURL( + mergeSearchParameters( + ...attributesToClear.map(({ helper: indexHelper, items }) => { + return clearRefinements({ + helper: indexHelper, + attributesToClear: items, + }); + }) + ) + ); + renderFn( { hasRefinements: attributesToClear.some( attributeToClear => attributeToClear.items.length > 0 ), - refine: () => { - attributesToClear.forEach(({ helper: indexHelper, items }) => { - indexHelper - .setState( - clearRefinements({ - helper: indexHelper, - attributesToClear: items, - }) - ) - .search(); - }); - }, - createURL: () => - createURL( - mergeSearchParameters( - ...attributesToClear.map(({ helper: indexHelper, items }) => { - return clearRefinements({ - helper: indexHelper, - attributesToClear: items, - }); - }) - ) - ), + refine: connectorState.refine, + createURL: connectorState.createURL, instantSearchInstance, widgetParams, }, From 72a39a6fb7f293c512ef3974d492e8a4ec514470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 28 Aug 2019 14:35:35 +0200 Subject: [PATCH 4/4] fix(clearRefinements): provide same function references for refine and createURL --- .../__tests__/connectClearRefinements-test.js | 43 ++++++++++++++++++- .../connectClearRefinements.js | 13 +++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js index 25f2f11176..8038c7fc43 100644 --- a/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js +++ b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js @@ -204,6 +204,46 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin expect(helper.state.query).toBe(''); }); + it('provides the same `refine` and `createURL` function references during the lifecycle', () => { + const helper = jsHelper({}, 'indexName'); + helper.search = () => {}; + + const rendering = jest.fn(); + const makeWidget = connectClearRefinements(rendering); + const widget = makeWidget({}); + + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + helper, + state: helper.state, + }) + ); + + const [firstRender, secondRender, thirdRender] = rendering.mock.calls; + + expect(secondRender[0].refine).toBe(firstRender[0].refine); + expect(thirdRender[0].refine).toBe(secondRender[0].refine); + + expect(secondRender[0].createURL).toBe(firstRender[0].createURL); + expect(thirdRender[0].createURL).toBe(secondRender[0].createURL); + }); + it('gets refinements from results', () => { const helper = jsHelper({}, undefined, { facets: ['aFacet'], @@ -615,10 +655,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/clear-refin results: new SearchResults(helper.state, [{}]), helper, state: helper.state, + createURL: state => state, }) ); - const { createURL, refine } = rendering.mock.calls[1][0]; + const { createURL, refine } = rendering.mock.calls[2][0]; const createURLState = createURL(); refine(); diff --git a/src/connectors/clear-refinements/connectClearRefinements.js b/src/connectors/clear-refinements/connectClearRefinements.js index c6fabdb955..fa2a3c4ba7 100644 --- a/src/connectors/clear-refinements/connectClearRefinements.js +++ b/src/connectors/clear-refinements/connectClearRefinements.js @@ -90,9 +90,12 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { const connectorState = { refine: noop, - createURL: noop, + createURL: () => '', }; + const cachedRefine = () => connectorState.refine(); + const cachedCreateURL = () => connectorState.createURL(); + return { $$type: 'ais.clearRefinements', @@ -100,8 +103,8 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { renderFn( { hasRefinements: false, - refine: connectorState.refine, - createURL: connectorState.createURL, + refine: cachedRefine, + createURL: cachedCreateURL, instantSearchInstance, widgetParams, }, @@ -154,8 +157,8 @@ export default function connectClearRefinements(renderFn, unmountFn = noop) { hasRefinements: attributesToClear.some( attributeToClear => attributeToClear.items.length > 0 ), - refine: connectorState.refine, - createURL: connectorState.createURL, + refine: cachedRefine, + createURL: cachedCreateURL, instantSearchInstance, widgetParams, },