diff --git a/packages/vue-instantsearch/package.json b/packages/vue-instantsearch/package.json index bbeefe2361..02dc834220 100644 --- a/packages/vue-instantsearch/package.json +++ b/packages/vue-instantsearch/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "algoliasearch-helper": "^3.1.0", - "instantsearch.js": "^4.16.1" + "instantsearch.js": "^4.20.0" }, "peerDependencies": { "algoliasearch": ">= 3.32.0 < 5", @@ -114,7 +114,7 @@ "bundlesize": [ { "path": "./dist/vue-instantsearch.js", - "maxSize": "52.50 kB" + "maxSize": "52.75 kB" }, { "path": "./dist/vue-instantsearch.common.js", diff --git a/packages/vue-instantsearch/src/components/InstantSearch.js b/packages/vue-instantsearch/src/components/InstantSearch.js index e5d54f0633..928f279898 100644 --- a/packages/vue-instantsearch/src/components/InstantSearch.js +++ b/packages/vue-instantsearch/src/components/InstantSearch.js @@ -69,6 +69,10 @@ export default createInstantSearchComponent({ return false; }, }, + middlewares: { + type: Array, + default: null, + }, }, data() { return { diff --git a/packages/vue-instantsearch/src/components/__tests__/InstantSearch-integration.js b/packages/vue-instantsearch/src/components/__tests__/InstantSearch-integration.js index b994f0c7e1..74ec59301e 100644 --- a/packages/vue-instantsearch/src/components/__tests__/InstantSearch-integration.js +++ b/packages/vue-instantsearch/src/components/__tests__/InstantSearch-integration.js @@ -1,10 +1,13 @@ +import Vue from 'vue'; import { mount } from '@vue/test-utils'; import InstantSearch from '../InstantSearch'; import { createWidgetMixin } from '../../mixins/widget'; import { createFakeClient } from '../../util/testutils/client'; - +import SearchBox from '../SearchBox.vue'; jest.unmock('instantsearch.js/es'); +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + it('child widgets get added to its parent instantsearch', () => { const widgetInstance = { render() {}, @@ -32,3 +35,154 @@ it('child widgets get added to its parent instantsearch', () => { widgetInstance ); }); + +describe('middlewares', () => { + const createFakeMiddleware = () => { + const middlewareSpy = { + onStateChange: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + }; + const middleware = jest.fn(() => middlewareSpy); + + return [middleware, middlewareSpy]; + }; + + it('subscribes middlewares', async () => { + const [middleware, middlewareSpy] = createFakeMiddleware(); + + mount(InstantSearch, { + propsData: { + searchClient: createFakeClient(), + indexName: 'indexName', + middlewares: [middleware], + }, + }); + await Vue.nextTick(); + + expect(middlewareSpy.subscribe).toHaveBeenCalledTimes(1); + }); + + it('subscribes newly added middleware', async () => { + const [middleware1, middlewareSpy1] = createFakeMiddleware(); + + const wrapper = mount({ + components: { + AisInstantSearch: InstantSearch, + AisSearchBox: SearchBox, + }, + template: ` + + + + `, + data() { + return { + searchClient: createFakeClient(), + indexName: 'indexName', + middlewares: [middleware1], + }; + }, + }); + + await wait(20); + expect(middlewareSpy1.subscribe).toHaveBeenCalledTimes(1); + + await wrapper.find('input').setValue('a'); + await Vue.nextTick(); + + expect(middlewareSpy1.onStateChange).toHaveBeenCalledTimes(1); + expect(middlewareSpy1.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'a' } }, + }); + + const [middleware2, middlewareSpy2] = createFakeMiddleware(); + wrapper.setData({ + middlewares: [middleware1, middleware2], + }); + await Vue.nextTick(); + + expect(middlewareSpy2.subscribe).toHaveBeenCalledTimes(1); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledTimes(0); + + await wrapper.find('input').setValue('b'); + await Vue.nextTick(); + + expect(middlewareSpy1.onStateChange).toHaveBeenCalledTimes(2); + expect(middlewareSpy1.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'b' } }, + }); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledTimes(1); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'b' } }, + }); + + expect(middlewareSpy1.unsubscribe).toHaveBeenCalledTimes(0); + expect(middlewareSpy2.unsubscribe).toHaveBeenCalledTimes(0); + }); + + it('unsubscribes removed middleware', async () => { + const [middleware1, middlewareSpy1] = createFakeMiddleware(); + const [middleware2, middlewareSpy2] = createFakeMiddleware(); + + const wrapper = mount({ + components: { + AisInstantSearch: InstantSearch, + AisSearchBox: SearchBox, + }, + template: ` + + + + `, + data() { + return { + searchClient: createFakeClient(), + indexName: 'indexName', + middlewares: [middleware1, middleware2], + }; + }, + }); + + await wait(20); + expect(middlewareSpy1.subscribe).toHaveBeenCalledTimes(1); + expect(middlewareSpy2.subscribe).toHaveBeenCalledTimes(1); + + await wrapper.find('input').setValue('a'); + await Vue.nextTick(); + + expect(middlewareSpy1.onStateChange).toHaveBeenCalledTimes(1); + expect(middlewareSpy1.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'a' } }, + }); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledTimes(1); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'a' } }, + }); + + wrapper.setData({ + middlewares: [middleware1], + }); + await Vue.nextTick(); + + expect(middlewareSpy1.unsubscribe).toHaveBeenCalledTimes(0); + expect(middlewareSpy2.unsubscribe).toHaveBeenCalledTimes(1); + + await wrapper.find('input').setValue('b'); + await Vue.nextTick(); + + expect(middlewareSpy1.onStateChange).toHaveBeenCalledTimes(2); + expect(middlewareSpy1.onStateChange).toHaveBeenCalledWith({ + uiState: { indexName: { query: 'b' } }, + }); + expect(middlewareSpy2.onStateChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/vue-instantsearch/src/util/createInstantSearchComponent.js b/packages/vue-instantsearch/src/util/createInstantSearchComponent.js index 3501d6c66c..5d3770dec4 100644 --- a/packages/vue-instantsearch/src/util/createInstantSearchComponent.js +++ b/packages/vue-instantsearch/src/util/createInstantSearchComponent.js @@ -34,6 +34,22 @@ export const createInstantSearchComponent = component => // private InstantSearch.js API: this.instantSearchInstance._searchFunction = searchFunction; }, + middlewares: { + immediate: true, + handler(next, prev) { + (prev || []) + .filter(middleware => (next || []).indexOf(middleware) === -1) + .forEach(middlewareToRemove => { + this.instantSearchInstance.unuse(middlewareToRemove); + }); + + (next || []) + .filter(middleware => (prev || []).indexOf(middleware) === -1) + .forEach(middlewareToAdd => { + this.instantSearchInstance.use(middlewareToAdd); + }); + }, + }, }, created() { const searchClient = this.instantSearchInstance.client;