diff --git a/src/connectors/clear-all/__tests__/connectClearAll-test.js b/src/connectors/clear-all/__tests__/connectClearAll-test.js index 6f67d3eb4d..6f8cef3023 100644 --- a/src/connectors/clear-all/__tests__/connectClearAll-test.js +++ b/src/connectors/clear-all/__tests__/connectClearAll-test.js @@ -8,7 +8,7 @@ const SearchResults = jsHelper.SearchResults; import connectClearAll from '../connectClearAll.js'; -describe.only('connectClearAll', () => { +describe('connectClearAll', () => { it('Renders during init and render', () => { const helper = jsHelper({}); helper.search = sinon.stub(); @@ -86,9 +86,6 @@ describe.only('connectClearAll', () => { const initClearMethod = rendering.lastCall.args[0].clearAll; initClearMethod(); - // The clearing function only works when results are received - // At this point of the lifecycle we don't have results yet - // that's why the search state still contains refinements expect(helper.hasRefinements('myFacet')).toBe(true); helper.toggleRefinement('myFacet', 'someOtherValue'); @@ -100,10 +97,10 @@ describe.only('connectClearAll', () => { createURL: () => '#', }); - // expect(helper.hasRefinements('myFacet')).toBe(true); - // const renderClearMethod = rendering.lastCall.args[0].clearAll; - // renderClearMethod(); - // expect(helper.hasRefinements('myFacet')).toBe(false); + expect(helper.hasRefinements('myFacet')).toBe(true); + const renderClearMethod = rendering.lastCall.args[0].clearAll; + renderClearMethod(); + expect(helper.hasRefinements('myFacet')).toBe(false); }); it('some refinements from results <=> hasRefinements = true', () => { @@ -127,7 +124,7 @@ describe.only('connectClearAll', () => { onHistoryChange: () => {}, }); - expect(rendering.lastCall.args[0].hasRefinements).toBe(false); + expect(rendering.lastCall.args[0].hasRefinements).toBe(true); widget.render({ results: new SearchResults(helper.state, [{}]), @@ -171,37 +168,4 @@ describe.only('connectClearAll', () => { expect(rendering.lastCall.args[0].hasRefinements).toBe(false); }); - - it('some refinements from results <=> hasRefinements = true', () => { - // test if the values sent to the rendering function - // are consistent with the search state - const helper = jsHelper({}, undefined, {facets: ['aFacet']}); - helper.toggleRefinement('aFacet', 'some value'); - helper.search = sinon.stub(); - - const container = document.createElement('div'); - const rendering = sinon.stub(); - const makeWidget = connectClearAll(rendering); - const widget = makeWidget({ - container, - }); - - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - onHistoryChange: () => {}, - }); - - expect(rendering.lastCall.args[0].hasRefinements).toBe(false); - - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - }); - - expect(rendering.lastCall.args[0].hasRefinements).toBe(true); - }); }); diff --git a/src/connectors/clear-all/connectClearAll.js b/src/connectors/clear-all/connectClearAll.js index 44ea410a2b..1c5ab482c9 100644 --- a/src/connectors/clear-all/connectClearAll.js +++ b/src/connectors/clear-all/connectClearAll.js @@ -1,6 +1,7 @@ import { bemHelper, getContainerNode, + getRefinements, prepareTemplateProps, clearRefinementsFromState, clearRefinementsAndSearch, @@ -51,13 +52,16 @@ const connectClearAll = renderClearAll => ({ init({helper, templatesConfig, createURL}) { this.clearAll = this.clearAll.bind(this, helper); this._templateProps = prepareTemplateProps({defaultTemplates, templatesConfig, templates}); - this.clearAttributes = []; + this.clearAttributes = getRefinements({}, helper.state) + .map(one => one.attributeName) + .filter(one => excludeAttributes.indexOf(one) === -1); + const hasRefinements = this.clearAttributes.length !== 0; renderClearAll({ clearAll: () => {}, collapsible, cssClasses, - hasRefinements: false, + hasRefinements, shouldAutoHideContainer: autoHideContainer, templateProps: this._templateProps, url: createURL(clearRefinementsFromState(helper.state)), @@ -66,7 +70,7 @@ const connectClearAll = renderClearAll => ({ }, render({results, state, createURL}) { - this.clearAttributes = results.getRefinements() + this.clearAttributes = getRefinements(results, state) .map(one => one.attributeName) .filter(one => excludeAttributes.indexOf(one) === -1); const hasRefinements = this.clearAttributes.length !== 0; diff --git a/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js b/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js new file mode 100644 index 0000000000..9ab1464b65 --- /dev/null +++ b/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js @@ -0,0 +1,111 @@ +/* eslint-env mocha */ + +import expect from 'expect'; +import sinon from 'sinon'; + +import jsHelper from 'algoliasearch-helper'; +const SearchResults = jsHelper.SearchResults; + +import connectCurrentRefinedValues from '../connectCurrentRefinedValues.js'; + +describe('connectCurrentRefinedValues', () => { + it('Renders during init and render', () => { + const helper = jsHelper({}); + helper.search = sinon.stub(); + const container = document.createElement('div'); + // test that the dummyRendering is called with the isFirstRendering + // flag set accordingly + const rendering = sinon.stub(); + const makeWidget = connectCurrentRefinedValues(rendering); + const widget = makeWidget({ + container, + }); + + expect(widget.getConfiguration).toBe(undefined); + // test if widget is not rendered yet at this point + expect(rendering.callCount).toBe(0); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + onHistoryChange: () => {}, + }); + + // test that rendering has been called during init with isFirstRendering = true + expect(rendering.callCount).toBe(1); + expect(rendering.lastCall.args[1]).toBe(true); + + const firstRenderingOptions = rendering.lastCall.args[0]; + expect(firstRenderingOptions.containerNode).toBe(container); + expect(firstRenderingOptions.refinements).toEqual([]); + expect(firstRenderingOptions.collapsible).toBe(false); + expect(firstRenderingOptions.shouldAutoHideContainer).toBe(true); + + widget.render({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + // 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.containerNode).toBe(container); + expect(secondRenderingOptions.refinements).toEqual([]); + expect(secondRenderingOptions.collapsible).toBe(false); + expect(secondRenderingOptions.shouldAutoHideContainer).toBe(true); + }); + + it('Provide a function to clear the refinements at each step', () => { + // For each refinements we get a function that we can call + // for removing a single refinement + const helper = jsHelper({}, '', { + facets: ['myFacet'], + }); + helper.search = sinon.stub(); + const container = document.createElement('div'); + const rendering = sinon.stub(); + const makeWidget = connectCurrentRefinedValues(rendering); + const widget = makeWidget({ + container, + }); + + helper.addFacetRefinement('myFacet', 'value'); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + onHistoryChange: () => {}, + }); + + const firstRenderingOptions = rendering.lastCall.args[0]; + const clearFunctions = firstRenderingOptions.clearRefinementClicks; + const refinements = firstRenderingOptions.refinements; + expect(clearFunctions.length).toBe(1); + expect(refinements.length).toBe(1); + clearFunctions[0](); + expect(helper.hasRefinements('myFacet')).toBe(false); + + helper.addFacetRefinement('myFacet', 'value'); + + widget.render({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + const secondRenderingOptions = rendering.lastCall.args[0]; + const otherClearFunctions = secondRenderingOptions.clearRefinementClicks; + const otherRefinements = secondRenderingOptions.refinements; + expect(otherClearFunctions.length).toBe(1); + expect(otherRefinements.length).toBe(1); + otherClearFunctions[0](); + expect(helper.hasRefinements('myFacet')).toBe(false); + }); +}); diff --git a/src/connectors/current-refined-values/connectCurrentRefinedValues.js b/src/connectors/current-refined-values/connectCurrentRefinedValues.js index e53e6d114c..99973d422d 100644 --- a/src/connectors/current-refined-values/connectCurrentRefinedValues.js +++ b/src/connectors/current-refined-values/connectCurrentRefinedValues.js @@ -166,17 +166,22 @@ const connectCurrentRefinedValues = renderCurrentRefinedValues => ({ const clearAllURL = createURL(clearRefinementsFromState(helper.state, restrictedTo)); + const refinements = getFilteredRefinements({}, helper.state, attributeNames, onlyListedAttributes); + const clearRefinementURLs = + refinements.map(refinement => createURL(clearRefinementFromState(helper.state, refinement))); + const clearRefinementClicks = refinements.map(refinement => clearRefinement.bind(null, helper, refinement)); + renderCurrentRefinedValues({ attributes: attributesObj, clearAllClick: this._clearRefinementsAndSearch, clearAllPosition: clearAll, clearAllURL, - clearRefinementClicks: [], - clearRefinementURLs: [], + clearRefinementClicks, + clearRefinementURLs, collapsible, cssClasses, - refinements: [], - autoHideContainer, + refinements, + shouldAutoHideContainer: autoHideContainer, templateProps: this._templateProps, containerNode, }, true); diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js new file mode 100644 index 0000000000..64ef0f9838 --- /dev/null +++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js @@ -0,0 +1,204 @@ +/* eslint-env mocha */ + +import expect from 'expect'; +import sinon from 'sinon'; + +import jsHelper from 'algoliasearch-helper'; +const SearchResults = jsHelper.SearchResults; + +import connectHierarchicalMenu from '../connectHierarchicalMenu.js'; + +describe('connectHierarchicalMenu', () => { + it('Renders during init and render', () => { + const container = document.createElement('div'); + // test that the dummyRendering is called with the isFirstRendering + // flag set accordingly + const rendering = sinon.stub(); + const makeWidget = connectHierarchicalMenu(rendering); + const widget = makeWidget({ + container, + attributes: ['category', 'sub_category'], + }); + + const config = widget.getConfiguration({}); + expect(config).toEqual({ + hierarchicalFacets: [ + { + attributes: ['category', 'sub_category'], + name: 'category', + rootPath: null, + separator: ' > ', + showParentLevel: true, + }, + ], + maxValuesPerFacet: 10, + }); + + // test if widget is not rendered yet at this point + expect(rendering.callCount).toBe(0); + + const helper = jsHelper({}, '', config); + helper.search = sinon.stub(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + onHistoryChange: () => {}, + }); + + // test that rendering has been called during init with isFirstRendering = true + expect(rendering.callCount).toBe(1); + expect(rendering.lastCall.args[1]).toBe(true); + + const firstRenderingOptions = rendering.lastCall.args[0]; + expect(firstRenderingOptions.containerNode).toBe(container); + expect(firstRenderingOptions.collapsible).toBe(false); + expect(firstRenderingOptions.shouldAutoHideContainer).toBe(true); + + widget.render({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + // 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.containerNode).toBe(container); + expect(secondRenderingOptions.collapsible).toBe(false); + expect(secondRenderingOptions.shouldAutoHideContainer).toBe(true); + }); + + it('Provide a function to clear the refinements at each step', () => { + const container = document.createElement('div'); + const rendering = sinon.stub(); + const makeWidget = connectHierarchicalMenu(rendering); + const widget = makeWidget({ + container, + attributes: ['category', 'sub_category'], + }); + + const helper = jsHelper({}, '', widget.getConfiguration({})); + helper.search = sinon.stub(); + + helper.toggleRefinement('category', 'value'); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + onHistoryChange: () => {}, + }); + + const firstRenderingOptions = rendering.lastCall.args[0]; + const {toggleRefinement} = firstRenderingOptions; + toggleRefinement('value'); + expect(helper.hasRefinements('category')).toBe(false); + toggleRefinement('value'); + expect(helper.hasRefinements('category')).toBe(true); + + widget.render({ + results: new SearchResults(helper.state, [{}, {}]), + state: helper.state, + helper, + createURL: () => '#', + }); + + const secondRenderingOptions = rendering.lastCall.args[0]; + const {toggleRefinement: renderToggleRefinement} = secondRenderingOptions; + renderToggleRefinement('value'); + expect(helper.hasRefinements('category')).toBe(false); + renderToggleRefinement('value'); + expect(helper.hasRefinements('category')).toBe(true); + }); + + it('provides the correct facet values', () => { + const container = document.createElement('div'); + const rendering = sinon.stub(); + const makeWidget = connectHierarchicalMenu(rendering); + const widget = makeWidget({ + container, + attributes: ['category', 'subCategory'], + }); + + const helper = jsHelper({}, '', widget.getConfiguration({})); + helper.search = sinon.stub(); + + helper.toggleRefinement('category', 'Decoration'); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + 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.facetValues).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.lastCall.args[0]; + expect(secondRenderingOptions.facetValues).toEqual([ + { + name: 'Decoration', + path: 'Decoration', + count: 880, + isRefined: true, + data: [ + { + name: 'Candle holders & candles', + path: 'Decoration > Candle holders & candles', + count: 193, + isRefined: false, + data: null, + }, + { + name: 'Frames & pictures', + path: 'Decoration > Frames & pictures', + count: 173, + isRefined: false, + data: null, + }, + ], + }, + { + name: 'Outdoor', + path: 'Outdoor', + count: 47, + isRefined: false, + data: null, + }, + ]); + }); +}); diff --git a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js index 13bb44597f..45dee43829 100644 --- a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js +++ b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js @@ -128,7 +128,7 @@ const connectHierarchicalMenu = renderHierarchicalMenu => ({ collapsible, createURL: _createURL, cssClasses, - facetValues: undefined, + facetValues: [], shouldAutoHideContainer: autoHideContainer, templateProps: this._templateProps, toggleRefinement: this._toggleRefinement,