From cbc1e1b382f0378bb027dc7313073eca3077bcc4 Mon Sep 17 00:00:00 2001 From: Vaillant Samuel Date: Fri, 20 Oct 2017 15:47:50 +0200 Subject: [PATCH] feat(RangeInput): add widget --- src/__tests__/index.js | 3 +- src/components/RangeInput.vue | 268 ++++ .../__snapshots__/range-input.js.snap | 109 ++ src/components/__tests__/range-input.js | 1079 +++++++++++++++++ src/instantsearch.js | 4 + stories/RangeInput.stories.js | 90 ++ 6 files changed, 1552 insertions(+), 1 deletion(-) create mode 100644 src/components/RangeInput.vue create mode 100644 src/components/__tests__/__snapshots__/range-input.js.snap create mode 100644 src/components/__tests__/range-input.js create mode 100644 stories/RangeInput.stories.js diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 0a577b752..48951b418 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -23,10 +23,11 @@ test('Should register all components when installed', () => { expect(component).toBeCalledWith('ais-search-box', expect.any(Object)); expect(component).toBeCalledWith('ais-clear', expect.any(Object)); expect(component).toBeCalledWith('ais-rating', expect.any(Object)); + expect(component).toBeCalledWith('ais-range-input', expect.any(Object)); expect(component).toBeCalledWith('ais-no-results', expect.any(Object)); expect(component).toBeCalledWith('ais-refinement-list', expect.any(Object)); expect(component).toBeCalledWith('ais-price-range', expect.any(Object)); expect(component).toBeCalledWith('ais-powered-by', expect.any(Object)); - expect(component).toHaveBeenCalledTimes(18); + expect(component).toHaveBeenCalledTimes(19); }); diff --git a/src/components/RangeInput.vue b/src/components/RangeInput.vue new file mode 100644 index 000000000..50742293a --- /dev/null +++ b/src/components/RangeInput.vue @@ -0,0 +1,268 @@ + + + diff --git a/src/components/__tests__/__snapshots__/range-input.js.snap b/src/components/__tests__/__snapshots__/range-input.js.snap new file mode 100644 index 000000000..ed71231cd --- /dev/null +++ b/src/components/__tests__/__snapshots__/range-input.js.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RangeInput render default 1`] = ` + +
+
+ + to + + +
+
+ +`; + +exports[`RangeInput render with max 1`] = ` + +
+
+ + to + + +
+
+ +`; + +exports[`RangeInput render with min 1`] = ` + +
+
+ + to + + +
+
+ +`; + +exports[`RangeInput render with precision 1`] = ` + +
+
+ + to + + +
+
+ +`; diff --git a/src/components/__tests__/range-input.js b/src/components/__tests__/range-input.js new file mode 100644 index 000000000..d8010cb52 --- /dev/null +++ b/src/components/__tests__/range-input.js @@ -0,0 +1,1079 @@ +import Vue from 'vue'; +import { FACET_OR } from '../../store'; +import RangeInput from '../RangeInput.vue'; + +describe('RangeInput', () => { + const attributeName = 'price'; + const createFakeStore = props => + Object.assign( + { + activeRefinements: [], + addFacet: jest.fn(), + removeFacet: jest.fn(), + addNumericRefinement: jest.fn(), + removeNumericRefinement: jest.fn(), + getFacetStats: jest.fn(() => ({})), + start: jest.fn(), + stop: jest.fn(), + refresh: jest.fn(), + }, + props + ); + + const render = propsData => { + const Component = Vue.extend(RangeInput); + const vm = new Component({ + propsData, + }); + + return vm.$mount(); + }; + + test('render default', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + expect(component.$el.outerHTML).toMatchSnapshot(); + expect(component.attributeName).toBe('price'); + }); + + test('render with values', () => { + const searchStore = createFakeStore({ + activeRefinements: [ + { + attributeName, + type: 'numeric', + operator: '>=', + numericValue: 10, + }, + { + attributeName, + type: 'numeric', + operator: '<=', + numericValue: 490, + }, + ], + }); + + const component = render({ + attributeName, + searchStore, + }); + + expect( + component.$el.querySelector('.ais-range-input__input--from').value + ).toBe('10'); + + expect( + component.$el.querySelector('.ais-range-input__input--to').value + ).toBe('490'); + }); + + test('render with min', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + min: 10, + searchStore, + }); + + expect(component.$el.outerHTML).toMatchSnapshot(); + expect(component.min).toBe(10); + }); + + test('render with max', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + max: 500, + searchStore, + }); + + expect(component.$el.outerHTML).toMatchSnapshot(); + expect(component.max).toBe(500); + }); + + test('render with precision', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + precision: 2, + searchStore, + }); + + expect(component.$el.outerHTML).toMatchSnapshot(); + expect(component.precision).toBe(2); + }); + + test('expect to call onSubmit when submit', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + const onSubmit = jest.spyOn(component, 'onSubmit'); + + const form = component.$el.querySelector('form'); + const event = new window.Event('submit'); + + form.dispatchEvent(event); + component._watcher.run(); + + expect(onSubmit).toHaveBeenCalled(); + + onSubmit.mockClear(); + onSubmit.mockReset(); + }); + + test('expect to set min when min input change', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + const input = component.$el.querySelector('.ais-range-input__input--from'); + const event = new window.Event('input'); + + input.value = 5; + + input.dispatchEvent(event); + component._watcher.run(); + + expect(component.refinement.min).toBe('5'); + }); + + test('expect to set max when max input change', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + const input = component.$el.querySelector('.ais-range-input__input--to'); + const event = new window.Event('input'); + + input.value = 10; + + input.dispatchEvent(event); + component._watcher.run(); + + expect(component.refinement.max).toBe('10'); + }); + + describe('created', () => { + test('expect to add the facet attribute', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + }); + + expect(searchStore.addFacet).toHaveBeenCalledWith('price', FACET_OR); + }); + + test('expect to refine min from default refinement', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + min: 10, + defaultRefinement: { + min: 20, + }, + }); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + 'price', + '>=', + 20 + ); + }); + + test('expect to refine max from default refinement', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + max: 500, + defaultRefinement: { + max: 490, + }, + }); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + 'price', + '<=', + 490 + ); + }); + + test('expect to refine min from bound', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + min: 10, + }); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + 'price', + '>=', + 10 + ); + }); + + test('expect to refine max from bound', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + max: 500, + }); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + 'price', + '<=', + 500 + ); + }); + + test('expect to call stop, start & refresh', () => { + const searchStore = createFakeStore(); + + render({ + attributeName, + searchStore, + }); + + expect(searchStore.start).toHaveBeenCalledTimes(1); + expect(searchStore.stop).toHaveBeenCalledTimes(1); + expect(searchStore.refresh).toHaveBeenCalledTimes(1); + }); + }); + + describe('destroyed', () => { + test('expect to remove the facet attribute', () => { + const searchStore = createFakeStore(); + + const component = render({ + attributeName, + searchStore, + }); + + component.$destroy(); + + expect(searchStore.removeFacet).toHaveBeenCalledWith('price'); + }); + }); + + describe('computed', () => { + describe('step', () => { + test('expect to return a step from a precision of 0', () => { + const searchStore = createFakeStore(); + + const component = render({ + attributeName, + searchStore, + precision: 0, + }); + + expect(component.step).toBe(1); + }); + + test('expect to return a step from a precision of 1', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + precision: 1, + }); + + expect(component.step).toBe(0.1); + }); + + test('expect to return a step from a precision of 2', () => { + const searchStore = createFakeStore(); + + const expectation = 0.01; + const actual = RangeInput.computed.step({ + attributeName, + searchStore, + precision: 2, + }); + + expect(actual).toBe(expectation); + }); + }); + + describe('refinement', () => { + test('expect to return the current refinement', () => { + const searchStore = createFakeStore({ + activeRefinements: [ + { + attributeName, + type: 'numeric', + operator: '>=', + numericValue: 10, + }, + { + attributeName, + type: 'numeric', + operator: '<=', + numericValue: 500, + }, + ], + }); + + const expectation = { min: 10, max: 500 }; + const actual = RangeInput.computed.refinement({ + attributeName, + searchStore, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return undefined when refinement is not set', () => { + const searchStore = createFakeStore(); + + const expectation = {}; + const actual = RangeInput.computed.refinement({ + attributeName, + searchStore, + }); + + expect(actual).toEqual(expectation); + }); + }); + + describe('range', () => { + test('expect to return the range from boundaries', () => { + const searchStore = createFakeStore(); + + const expectation = { + min: 10, + max: 500, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 0, + min: 10, + max: 500, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the range from stats', () => { + const searchStore = createFakeStore({ + getFacetStats: jest.fn(() => ({ + min: 0, + max: 799, + })), + }); + + const expectation = { + min: 0, + max: 799, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 0, + }); + + expect(actual).toEqual(expectation); + expect(searchStore.getFacetStats).toHaveBeenCalledWith(attributeName); + }); + + test('expect to return the default range', () => { + const searchStore = createFakeStore(); + + const expectation = { + min: -Infinity, + max: Infinity, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 0, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the range with a precision of 0', () => { + const searchStore = createFakeStore({ + getFacetStats: () => ({ + min: 10.1234, + max: 799.5678, + }), + }); + + const expectation = { + min: 10, + max: 800, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 0, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the range with a precision of 1', () => { + const searchStore = createFakeStore({ + getFacetStats: () => ({ + min: 10.1234, + max: 799.5678, + }), + }); + + const expectation = { + min: 10.1, + max: 799.6, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 1, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the range with a precision of 2', () => { + const searchStore = createFakeStore({ + getFacetStats: () => ({ + min: 10.1234, + max: 799.5678, + }), + }); + + const expectation = { + min: 10.12, + max: 799.57, + }; + + const actual = RangeInput.computed.range({ + attributeName, + searchStore, + precision: 2, + }); + + expect(actual).toEqual(expectation); + }); + }); + + describe('rangeForRendering', () => { + test('expect to return the given range when both value are different from Infinity', () => { + const range = { + min: 0, + max: 500, + }; + + const expectation = { + min: 0, + max: 500, + }; + + const actual = RangeInput.computed.rangeForRendering({ + range, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return an empty string as range when min value is -Infinity', () => { + const range = { + min: -Infinity, + max: 500, + }; + + const expectation = { + min: '', + max: '', + }; + + const actual = RangeInput.computed.rangeForRendering({ + range, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return an empty string as range when max value is Infinity', () => { + const range = { + min: 0, + max: Infinity, + }; + + const expectation = { + min: '', + max: '', + }; + + const actual = RangeInput.computed.rangeForRendering({ + range, + }); + + expect(actual).toEqual(expectation); + }); + }); + + describe('refinementForRendering', () => { + test('expect to return the refinement when values are defined & differ from range', () => { + const range = { + min: 0, + max: 500, + }; + + const refinement = { + min: 10, + max: 490, + }; + + const expectation = { + min: 10, + max: 490, + }; + + const actual = RangeInput.computed.refinementForRendering({ + range, + refinement, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the min refinement as empty string when value is not defined', () => { + const range = { + min: 0, + max: 500, + }; + + const refinement = { + min: undefined, + max: 490, + }; + + const expectation = { + min: '', + max: 490, + }; + + const actual = RangeInput.computed.refinementForRendering({ + range, + refinement, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the max refinement as empty string when value is not defined', () => { + const range = { + min: 0, + max: 500, + }; + + const refinement = { + min: 10, + max: undefined, + }; + + const expectation = { + min: 10, + max: '', + }; + + const actual = RangeInput.computed.refinementForRendering({ + range, + refinement, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the min refinement as empty string when value is equal to range', () => { + const range = { + min: 0, + max: 500, + }; + + const refinement = { + min: 0, + max: 490, + }; + + const expectation = { + min: '', + max: 490, + }; + + const actual = RangeInput.computed.refinementForRendering({ + range, + refinement, + }); + + expect(actual).toEqual(expectation); + }); + + test('expect to return the max refinement as empty string when value is equal to range', () => { + const range = { + min: 0, + max: 500, + }; + + const refinement = { + min: 10, + max: 500, + }; + + const expectation = { + min: 10, + max: '', + }; + + const actual = RangeInput.computed.refinementForRendering({ + range, + refinement, + }); + + expect(actual).toEqual(expectation); + }); + }); + }); + + describe('methods', () => { + describe('onSubmit', () => { + test('expect to refine min value', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 10, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 10 + ); + }); + + test('expect to refine max value', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + max: 490, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 490 + ); + }); + + test('expect to refine min value with float number', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 10.1234, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 10.1234 + ); + }); + + test('expect to refine max value with float number', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + max: 489.5678, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 489.5678 + ); + }); + + test('expect to refine min value with parsable number', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: '10', + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 10 + ); + }); + + test('expect to refine max value with parsable number', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + max: '490', + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 490 + ); + }); + + test('expect to refine min value when value is equal to min range & bound are defined', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + min: 10, + }); + + component.onSubmit({ + min: 10, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 10 + ); + }); + + test('expect to refine max value when value is equal to max range & bound are defined', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + max: 490, + }); + + component.onSubmit({ + max: 490, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator, + 490 + ); + }); + + test('expect to reset min value when value is not defined', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + max: 490, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '<=', + 490 + ); + }); + + test('expect to reset max value when value is not defined', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 10, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '>=', + 10 + ); + }); + + test('expect to reset min value when value is an empty string', () => { + const operator = '>='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: '', + max: 490, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '<=', + 490 + ); + }); + + test('expect to reset max value when value is an empty string', () => { + const operator = '<='; + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 10, + max: '', + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '>=', + 10 + ); + }); + + test('expect to reset min value when value is equal to min range & bound are not defined', () => { + const operator = '>='; + + const searchStore = createFakeStore({ + getFacetStats: () => ({ + min: 0, + max: 799, + }), + }); + + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 0, + max: 250, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '<=', + 250 + ); + }); + + test('expect to reset max value when value is equal to max range & bound are not defined', () => { + const operator = '<='; + + const searchStore = createFakeStore({ + getFacetStats: () => ({ + min: 0, + max: 799, + }), + }); + + const component = render({ + attributeName, + searchStore, + }); + + component.onSubmit({ + min: 10, + max: 799, + }); + + expect(searchStore.removeNumericRefinement).toHaveBeenCalledWith( + attributeName, + operator + ); + + expect(searchStore.addNumericRefinement).not.toHaveBeenCalledWith( + attributeName, + operator, + undefined + ); + + expect(searchStore.addNumericRefinement).toHaveBeenCalledWith( + attributeName, + '>=', + 10 + ); + }); + + test('expect to call stop, start & refresh', () => { + const searchStore = createFakeStore(); + const component = render({ + attributeName, + searchStore, + }); + + searchStore.stop.mockClear(); + searchStore.start.mockClear(); + searchStore.refresh.mockClear(); + + component.onSubmit({}); + component.onSubmit({}); + + expect(searchStore.stop).toHaveBeenCalledTimes(2); + expect(searchStore.start).toHaveBeenCalledTimes(2); + expect(searchStore.refresh).toHaveBeenCalledTimes(2); + }); + }); + }); +}); diff --git a/src/instantsearch.js b/src/instantsearch.js index 4c619ec11..d5c9b4fb6 100644 --- a/src/instantsearch.js +++ b/src/instantsearch.js @@ -24,6 +24,7 @@ import SortBySelector from './components/SortBySelector.vue'; import SearchBox from './components/SearchBox.vue'; import Clear from './components/Clear.vue'; import Rating from './components/Rating.vue'; +import RangeInput from './components/RangeInput.vue'; import NoResults from './components/NoResults.vue'; import RefinementList from './components/RefinementList.vue'; import PriceRange from './components/PriceRange.vue'; @@ -44,6 +45,7 @@ const InstantSearch = { SearchBox, Clear, Rating, + RangeInput, NoResults, RefinementList, PriceRange, @@ -64,6 +66,7 @@ const InstantSearch = { Vue.component('ais-search-box', SearchBox); Vue.component('ais-clear', Clear); Vue.component('ais-rating', Rating); + Vue.component('ais-range-input', RangeInput); Vue.component('ais-no-results', NoResults); Vue.component('ais-refinement-list', RefinementList); Vue.component('ais-price-range', PriceRange); @@ -96,6 +99,7 @@ export { SearchBox, Clear, Rating, + RangeInput, NoResults, RefinementList, PriceRange, diff --git a/stories/RangeInput.stories.js b/stories/RangeInput.stories.js new file mode 100644 index 000000000..f4a331a0a --- /dev/null +++ b/stories/RangeInput.stories.js @@ -0,0 +1,90 @@ +import { previewWrapper } from './utils'; +import { storiesOf } from '@storybook/vue'; + +storiesOf('RangeInput', module) + .addDecorator(previewWrapper) + .add('default', () => ({ + template: ` + + `, + })) + .add('with precision', () => ({ + template: ` + + `, + })) + .add('with default refinement', () => ({ + template: ` + + `, + })) + .add('with min boundaries', () => ({ + template: ` + + `, + })) + .add('with max boundaries', () => ({ + template: ` + + `, + })) + .add('with min / max boundaries', () => ({ + template: ` + + `, + })) + .add('with min / max boundaries & default refinement', () => ({ + template: ` + + `, + })) + .add('with separator', () => ({ + template: ` + + --> + + `, + })) + .add('with header', () => ({ + template: ` + + Custom header + + `, + })) + .add('with footer', () => ({ + template: ` + + Custom footer + + `, + }));