From c5fc3f98a3e9955d8917fdc2b555b85b807ad272 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Fri, 31 Jul 2020 19:49:10 +0200 Subject: [PATCH 01/23] add tests for userToken --- src/middleware/__tests__/insights.ts | 191 +++++++++++++++++++++++++++ src/middleware/insights.ts | 18 ++- test/mock/createInsightsClient.ts | 68 ++++++++++ test/mock/createInstantSearch.ts | 2 +- 4 files changed, 272 insertions(+), 7 deletions(-) create mode 100644 src/middleware/__tests__/insights.ts create mode 100644 test/mock/createInsightsClient.ts diff --git a/src/middleware/__tests__/insights.ts b/src/middleware/__tests__/insights.ts new file mode 100644 index 0000000000..ff95e91977 --- /dev/null +++ b/src/middleware/__tests__/insights.ts @@ -0,0 +1,191 @@ +import algoliasearch from 'algoliasearch'; +import algoliasearchHelper from 'algoliasearch-helper'; +import { createInsightsMiddleware } from '../insights'; +import { createInstantSearch } from '../../../test/mock/createInstantSearch'; +import { + createInsightsClient, + createInsightsUmdVersion, + ANONYMOUS_TOKEN, +} from '../../../test/mock/createInsightsClient'; +import { warning } from '../../../src/lib/utils'; + +describe('insights', () => { + let insightsClient; + let instantSearchInstance; + let helper; + + beforeEach(() => { + insightsClient = createInsightsClient(); + instantSearchInstance = createInstantSearch({ + client: algoliasearch('myAppId', 'myApiKey'), + }); + helper = algoliasearchHelper({}, ''); + instantSearchInstance.mainIndex = { + getHelper: () => helper, + }; + warning.cache = {}; + }); + + describe('usage', () => { + it('throws when insightsClient is not given', () => { + expect(() => + // @ts-ignore:next-line + createInsightsMiddleware() + ).toThrowErrorMatchingInlineSnapshot( + `"The \`insightsClient\` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to \`false\`."` + ); + }); + + it('passes with insightsClient: false', () => { + expect(() => + createInsightsMiddleware({ + insightsClient: false, + }) + ).not.toThrow(); + }); + }); + + describe('initialize', () => { + it('initialize insightsClient', () => { + expect.assertions(3); + + insightsClient('setUserToken', 'abc'); + createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + insightsClient('_get', '_hasCredentials', hasCredentials => { + expect(hasCredentials).toBe(true); + }); + insightsClient('_get', '_appId', appId => { + expect(appId).toBe('myAppId'); + }); + insightsClient('_get', '_apiKey', apiKey => { + expect(apiKey).toBe('myApiKey'); + }); + }); + + it('warns dev if userToken is set before creating the middleware', () => { + insightsClient('setUserToken', 'abc'); + expect(() => { + createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + }) + .toWarnDev(`[InstantSearch.js]: You set userToken before \`createInsightsMiddleware()\` and it is ignored. +Please set the token after the \`createInsightsMiddleware()\` call. + +createInsightsMiddleware({ /* ... */ }); + +insightsClient('setUserToken', 'your-user-token'); +// or +aa('setUserToken', 'your-user-token');`); + }); + + it('applies clickAnalytics', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.clickAnalytics).toBe(true); + }); + }); + + describe('userToken', () => { + it('applies userToken which was set before subscribe()', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + insightsClient('setUserToken', 'abc'); + middleware.subscribe(); + expect(helper.state.userToken).toEqual('abc'); + }); + + it('applies userToken which was set after subscribe()', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + insightsClient('setUserToken', 'def'); + expect(helper.state.userToken).toEqual('def'); + }); + + it('applies userToken from cookie when nothing given', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + }); + + it('applies userToken from queue if exists', () => { + const { + insightsClient: localInsightsClient, + libraryLoadedAndProcessQueue, + } = createInsightsUmdVersion(); + + // call init and setUserToken even before the library is loaded. + localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + localInsightsClient('setUserToken', 'token-from-queue'); + libraryLoadedAndProcessQueue(); + + localInsightsClient('_get', '_userToken', userToken => { + expect(userToken).toEqual('token-from-queue'); + }); + + const middleware = createInsightsMiddleware({ + insightsClient: localInsightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual('token-from-queue'); + }); + + it('applies userToken from queue even though the queue is not processed', () => { + const { + insightsClient: localInsightsClient, + } = createInsightsUmdVersion(); + + // call init and setUserToken even before the library is loaded. + localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + localInsightsClient('setUserToken', 'token-from-queue'); + + localInsightsClient('_get', '_userToken', userToken => { + expect(userToken).toEqual('token-from-queue'); + }); + + const middleware = createInsightsMiddleware({ + insightsClient: localInsightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual('token-from-queue'); + }); + + it('ignores userToken set before init (umd)', () => { + const { + insightsClient: localInsightsClient, + libraryLoadedAndProcessQueue, + } = createInsightsUmdVersion(); + + localInsightsClient('setUserToken', 'token-from-queue-before-init'); + localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + libraryLoadedAndProcessQueue(); + + const middleware = createInsightsMiddleware({ + insightsClient: localInsightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + }); + + it('ignores userToken set before init (cjs)', () => { + insightsClient('setUserToken', 'token-from-queue-before-init'); + + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + }); + }); + + // describe('sendEvent', () => {}); +}); diff --git a/src/middleware/insights.ts b/src/middleware/insights.ts index 7cf2cd0fa0..3f021ce18c 100644 --- a/src/middleware/insights.ts +++ b/src/middleware/insights.ts @@ -17,7 +17,7 @@ export type InsightsProps = { export type CreateInsightsMiddleware = (props: InsightsProps) => Middleware; export const createInsightsMiddleware: CreateInsightsMiddleware = props => { - const { insightsClient: _insightsClient, onEvent } = props; + const { insightsClient: _insightsClient, onEvent } = props || {}; if (_insightsClient !== false && !_insightsClient) { if (__DEV__) { throw new Error( @@ -83,11 +83,17 @@ aa('setUserToken', 'your-user-token'); if (Array.isArray((insightsClient as any).queue)) { // Context: The umd build of search-insights is asynchronously loaded by the snippet. // - // When user called `aa('setUserToken', 'my-user-token')` before `search-insights` is loaded, - // it is stored in `aa.queue` and we are reading it to set userToken to search call. - // This queue is meant to be consumed whenever `search-insights` is loaded and when it runs `processQueue()`. - // But the reason why we handle it here is to prevent the first search API from being triggered - // without userToken because search-insights is not loaded yet. + // When user calls `aa('setUserToken', 'my-user-token')` before `search-insights` is loaded, + // ['setUserToken', 'my-user-token'] gets stored in `aa.queue`. + // Whenever `search-insights` is finally loaded, it will process the queue. + // + // But the reason why we handle it here is + // (1) At this point, even though `search-insights` is not loaded yet, + // we still want to read the token from the queue. + // Otherwise, the first search call will be fired without the token. + // (2) Or, user could use a customized client of `search-insights`, + // for example, by using `createInsightsClient` function. + // Then `processQueue` might not be called. But we still want to read the token from the queue. (insightsClient as any).queue.forEach(([method, firstArgument]) => { if (method === 'setUserToken') { setUserTokenToSearch(firstArgument); diff --git a/test/mock/createInsightsClient.ts b/test/mock/createInsightsClient.ts new file mode 100644 index 0000000000..ee48ea1583 --- /dev/null +++ b/test/mock/createInsightsClient.ts @@ -0,0 +1,68 @@ +export const ANONYMOUS_TOKEN = 'anonymous-user-id-1'; + +export function createAlgoliaAnalytics() { + let values = {}; + const setValues = obj => { + values = { + ...values, + ...obj, + }; + }; + let userTokenCallback; + const setUserToken = userToken => { + setValues({ _userToken: userToken }); + if (userTokenCallback) { + userTokenCallback(userToken); + } + }; + const init = ({ appId, apiKey }) => { + setValues({ _hasCredentials: true, _appId: appId, _apiKey: apiKey }); + setUserToken(ANONYMOUS_TOKEN); + }; + const _get = (key, callback) => callback(values[key]); + const onUserTokenChange = (callback, { immediate = false } = {}) => { + userTokenCallback = callback; + if (immediate) { + callback(values._userToken); + } + }; + + return { + setUserToken, + init, + _get, + onUserTokenChange, + }; +} + +export function createInsightsClient(instance = createAlgoliaAnalytics()) { + return (methodName, ...args) => { + if (!instance[methodName]) { + throw new Error(`${methodName} doesn't exist in this mocked instance`); + } + instance[methodName](...args); + }; +} + +export function createInsightsUmdVersion() { + const globalObject: any = {}; + globalObject.aa = (...args) => { + globalObject.aa.queue = globalObject.aa.queue || []; + globalObject.aa.queue.push(args); + }; + + return { + insightsClient: globalObject.aa, + libraryLoadedAndProcessQueue: () => { + const instance = createAlgoliaAnalytics(); + const _aa = createInsightsClient(instance); + const queue = globalObject.aa.queue; + queue.forEach(([methodName, ...args]) => { + _aa(methodName, ...args); + }); + queue.push = ([methodName, ...args]) => { + _aa(methodName, ...args); + }; + }, + }; +} diff --git a/test/mock/createInstantSearch.ts b/test/mock/createInstantSearch.ts index 581dda1b64..ec07c5feb5 100644 --- a/test/mock/createInstantSearch.ts +++ b/test/mock/createInstantSearch.ts @@ -7,7 +7,7 @@ import defer from '../../src/lib/utils/defer'; export const createInstantSearch = ( args: Partial = {} ): InstantSearch => { - const searchClient = createSearchClient(); + const searchClient = args.client || createSearchClient(); const { indexName = 'indexName' } = args; const mainHelper = algoliasearchHelper(searchClient, indexName, {}); const mainIndex = index({ indexName }); From d1ee53bc89fa0848771ce4b850fdfca7d2b8db02 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 24 Aug 2020 17:01:25 +0200 Subject: [PATCH 02/23] fix import paths --- src/lib/insights/listener.tsx | 2 +- src/lib/utils/createSendEventForHits.ts | 2 +- src/widgets/hits/hits.tsx | 2 +- src/widgets/infinite-hits/infinite-hits.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/insights/listener.tsx b/src/lib/insights/listener.tsx index 194abe571d..d645813e75 100644 --- a/src/lib/insights/listener.tsx +++ b/src/lib/insights/listener.tsx @@ -3,7 +3,7 @@ import { h } from 'preact'; import { readDataAttributes, hasDataAttributes } from '../../helpers/insights'; import { InsightsClientWrapper } from '../../types'; -import { InsightsEvent } from '../../middleware/insights'; +import { InsightsEvent } from '../../middlewares/insights'; type WithInsightsListenerProps = { [key: string]: unknown; diff --git a/src/lib/utils/createSendEventForHits.ts b/src/lib/utils/createSendEventForHits.ts index 53d3ba9570..19e4c6aa91 100644 --- a/src/lib/utils/createSendEventForHits.ts +++ b/src/lib/utils/createSendEventForHits.ts @@ -1,5 +1,5 @@ import { InstantSearch, Hit } from '../../types'; -import { InsightsEvent } from '../../middleware/insights'; +import { InsightsEvent } from '../../middlewares/insights'; type BuiltInSendEventForHits = ( eventType: string, diff --git a/src/widgets/hits/hits.tsx b/src/widgets/hits/hits.tsx index 3847b79156..3f0a7962ce 100644 --- a/src/widgets/hits/hits.tsx +++ b/src/widgets/hits/hits.tsx @@ -22,7 +22,7 @@ import { Renderer, InsightsClientWrapper, } from '../../types'; -import { InsightsEvent } from '../../middleware/insights'; +import { InsightsEvent } from '../../middlewares/insights'; const withUsage = createDocumentationMessageGenerator({ name: 'hits' }); const suit = component('Hits'); diff --git a/src/widgets/infinite-hits/infinite-hits.tsx b/src/widgets/infinite-hits/infinite-hits.tsx index 7c1f678f29..123f79a16b 100644 --- a/src/widgets/infinite-hits/infinite-hits.tsx +++ b/src/widgets/infinite-hits/infinite-hits.tsx @@ -24,7 +24,7 @@ import { Renderer, } from '../../types'; import defaultTemplates from './defaultTemplates'; -import { InsightsEvent } from '../../middleware/insights'; +import { InsightsEvent } from '../../middlewares/insights'; const withUsage = createDocumentationMessageGenerator({ name: 'infinite-hits', From aa24a78c64540628e7650866eb32515320dc5881 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 24 Aug 2020 17:02:08 +0200 Subject: [PATCH 03/23] fix test cases to accept null instead of false as insightsClient --- src/middlewares/__tests__/insights.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index ff95e91977..131438d8ad 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -32,14 +32,14 @@ describe('insights', () => { // @ts-ignore:next-line createInsightsMiddleware() ).toThrowErrorMatchingInlineSnapshot( - `"The \`insightsClient\` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to \`false\`."` + `"The \`insightsClient\` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to \`null\`."` ); }); - it('passes with insightsClient: false', () => { + it('passes with insightsClient: null', () => { expect(() => createInsightsMiddleware({ - insightsClient: false, + insightsClient: null, }) ).not.toThrow(); }); From a6b745e76f0aeb3d9a5f11cab544f71cdda7f805 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 24 Aug 2020 17:29:39 +0200 Subject: [PATCH 04/23] update bundlesize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1cc0b5dd9..44f0d2b304 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "bundlesize": [ { "path": "./dist/instantsearch.production.min.js", - "maxSize": "64 kB" + "maxSize": "66 kB" }, { "path": "./dist/instantsearch.development.js", From e5c88d38784c56bfe4720286ae55f1348570c7dc Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 24 Aug 2020 17:32:55 +0200 Subject: [PATCH 05/23] fix lint errors --- src/middlewares/__tests__/insights.ts | 3 ++- test/mock/createInsightsClient.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index 131438d8ad..4cd014a3cf 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -8,6 +8,7 @@ import { ANONYMOUS_TOKEN, } from '../../../test/mock/createInsightsClient'; import { warning } from '../../../src/lib/utils'; +import { SearchClient } from '../../types'; describe('insights', () => { let insightsClient; @@ -19,7 +20,7 @@ describe('insights', () => { instantSearchInstance = createInstantSearch({ client: algoliasearch('myAppId', 'myApiKey'), }); - helper = algoliasearchHelper({}, ''); + helper = algoliasearchHelper({} as SearchClient, ''); instantSearchInstance.mainIndex = { getHelper: () => helper, }; diff --git a/test/mock/createInsightsClient.ts b/test/mock/createInsightsClient.ts index ee48ea1583..39b38c70bf 100644 --- a/test/mock/createInsightsClient.ts +++ b/test/mock/createInsightsClient.ts @@ -1,7 +1,7 @@ export const ANONYMOUS_TOKEN = 'anonymous-user-id-1'; export function createAlgoliaAnalytics() { - let values = {}; + let values: any = {}; const setValues = obj => { values = { ...values, From 3abcef5ee7f7dc500853ec87d99e81442e8227c3 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 24 Aug 2020 18:03:47 +0200 Subject: [PATCH 06/23] test sendEventToInsights --- src/middlewares/__tests__/insights.ts | 66 ++++++++++++++++++++++++++- test/mock/createInsightsClient.ts | 1 + 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index 4cd014a3cf..e1714255b4 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -3,6 +3,7 @@ import algoliasearchHelper from 'algoliasearch-helper'; import { createInsightsMiddleware } from '../insights'; import { createInstantSearch } from '../../../test/mock/createInstantSearch'; import { + createAlgoliaAnalytics, createInsightsClient, createInsightsUmdVersion, ANONYMOUS_TOKEN, @@ -11,12 +12,14 @@ import { warning } from '../../../src/lib/utils'; import { SearchClient } from '../../types'; describe('insights', () => { + let analytics; let insightsClient; let instantSearchInstance; let helper; beforeEach(() => { - insightsClient = createInsightsClient(); + analytics = createAlgoliaAnalytics(); + insightsClient = jest.fn(createInsightsClient(analytics)); instantSearchInstance = createInstantSearch({ client: algoliasearch('myAppId', 'myApiKey'), }); @@ -188,5 +191,64 @@ aa('setUserToken', 'your-user-token');`); }); }); - // describe('sendEvent', () => {}); + describe('sendEventToInsights', () => { + it('sends events', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + + instantSearchInstance.sendEventToInsights({ + insightsMethod: 'viewedObjectIDs', + payload: { + hello: 'world', + }, + }); + expect(analytics.viewedObjectIDs).toHaveBeenCalledTimes(1); + expect(analytics.viewedObjectIDs).toHaveBeenCalledWith({ + hello: 'world', + }); + }); + + it('calls onEvent when given', () => { + const onEvent = jest.fn(); + const middleware = createInsightsMiddleware({ + insightsClient, + onEvent, + })({ instantSearchInstance }); + middleware.subscribe(); + + instantSearchInstance.sendEventToInsights({ + insightsMethod: 'viewedObjectIDs', + payload: { + hello: 'world', + }, + }); + expect(analytics.viewedObjectIDs).toHaveBeenCalledTimes(0); + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + insightsMethod: 'viewedObjectIDs', + payload: { + hello: 'world', + }, + }); + }); + + it('warns dev when neither insightsMethod nor onEvent is given', () => { + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + + const numberOfCalls = insightsClient.mock.calls.length; + expect(() => { + instantSearchInstance.sendEventToInsights({ + payload: { + hello: 'world', + }, + }); + }).toWarnDev(); + expect(insightsClient).toHaveBeenCalledTimes(numberOfCalls); // still the same + }); + }); }); diff --git a/test/mock/createInsightsClient.ts b/test/mock/createInsightsClient.ts index 39b38c70bf..b4cff2c4d5 100644 --- a/test/mock/createInsightsClient.ts +++ b/test/mock/createInsightsClient.ts @@ -32,6 +32,7 @@ export function createAlgoliaAnalytics() { init, _get, onUserTokenChange, + viewedObjectIDs: jest.fn(), }; } From 48d5cda562c3663a6a19a345085c8f282a391692 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Tue, 25 Aug 2020 15:32:26 +0200 Subject: [PATCH 07/23] use $$type instead of hard-code --- src/connectors/autocomplete/connectAutocomplete.ts | 2 +- src/connectors/breadcrumb/connectBreadcrumb.ts | 2 +- src/connectors/geo-search/connectGeoSearch.js | 6 ++++-- src/connectors/hierarchical-menu/connectHierarchicalMenu.js | 2 +- src/connectors/infinite-hits/connectInfiniteHits.ts | 4 ++-- src/connectors/menu/connectMenu.js | 2 +- src/connectors/numeric-menu/connectNumericMenu.ts | 2 +- src/connectors/range/connectRange.js | 6 ++++-- src/connectors/rating-menu/connectRatingMenu.js | 2 +- src/connectors/toggle-refinement/connectToggleRefinement.js | 2 +- 10 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/connectors/autocomplete/connectAutocomplete.ts b/src/connectors/autocomplete/connectAutocomplete.ts index 120316752d..bcb898f8a2 100644 --- a/src/connectors/autocomplete/connectAutocomplete.ts +++ b/src/connectors/autocomplete/connectAutocomplete.ts @@ -137,7 +137,7 @@ search.addWidgets([ const sendEvent = createSendEventForHits({ instantSearchInstance, index: scopedResult.results.index, - widgetType: 'ais.autocomplete', + widgetType: this.$$type!, }); sendEvent('view', scopedResult.results.hits); diff --git a/src/connectors/breadcrumb/connectBreadcrumb.ts b/src/connectors/breadcrumb/connectBreadcrumb.ts index 50ff8449b1..29587544db 100644 --- a/src/connectors/breadcrumb/connectBreadcrumb.ts +++ b/src/connectors/breadcrumb/connectBreadcrumb.ts @@ -121,7 +121,7 @@ const connectBreadcrumb: BreadcrumbConnector = function connectBreadcrumb( instantSearchInstance, helper, attribute: hierarchicalFacetName, - widgetType: 'ais.breadcrumb', + widgetType: this.$$type!, }); connectorState.createURL = facetValue => { diff --git a/src/connectors/geo-search/connectGeoSearch.js b/src/connectors/geo-search/connectGeoSearch.js index 5bbc54a4dc..d75f9bf36b 100644 --- a/src/connectors/geo-search/connectGeoSearch.js +++ b/src/connectors/geo-search/connectGeoSearch.js @@ -12,6 +12,8 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const $$type = 'ais.geoSearch'; + /** * @typedef {Object} LatLng * @property {number} lat The latitude in degrees. @@ -177,7 +179,7 @@ const connectGeoSearch = (renderFn, unmountFn = noop) => { sendEvent = createSendEventForHits({ instantSearchInstance, index: helper.getIndex(), - widgetType: 'ais.geoSearch', + widgetType: $$type, }); widgetState.internalToggleRefineOnMapMove = createInternalToggleRefinementOnMapMove( @@ -269,7 +271,7 @@ const connectGeoSearch = (renderFn, unmountFn = noop) => { }; return { - $$type: 'ais.geoSearch', + $$type, init, diff --git a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js index 7cfb377376..91f564cf66 100644 --- a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js +++ b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js @@ -120,7 +120,7 @@ export default function connectHierarchicalMenu(renderFn, unmountFn = noop) { instantSearchInstance, helper, attribute: hierarchicalFacetName, - widgetType: 'ais.refinementList', + widgetType: this.$$type, }); this.cachedToggleShowMore = this.cachedToggleShowMore.bind(this); diff --git a/src/connectors/infinite-hits/connectInfiniteHits.ts b/src/connectors/infinite-hits/connectInfiniteHits.ts index 52891e7b01..922a10a921 100644 --- a/src/connectors/infinite-hits/connectInfiniteHits.ts +++ b/src/connectors/infinite-hits/connectInfiniteHits.ts @@ -200,11 +200,11 @@ const connectInfiniteHits: InfiniteHitsConnector = function connectInfiniteHits( sendEvent = createSendEventForHits({ instantSearchInstance, index: helper.getIndex(), - widgetType: 'ais.infiniteHits', + widgetType: this.$$type!, }); bindEvent = createBindEventForHits({ index: helper.getIndex(), - widgetType: 'ais.infiniteHits', + widgetType: this.$$type!, }); renderFn( diff --git a/src/connectors/menu/connectMenu.js b/src/connectors/menu/connectMenu.js index 6c56d40b22..bfb129dfec 100644 --- a/src/connectors/menu/connectMenu.js +++ b/src/connectors/menu/connectMenu.js @@ -157,7 +157,7 @@ export default function connectMenu(renderFn, unmountFn = noop) { instantSearchInstance, helper, attribute, - widgetType: 'ais.menu', + widgetType: this.$$type, }); this.cachedToggleShowMore = this.cachedToggleShowMore.bind(this); diff --git a/src/connectors/numeric-menu/connectNumericMenu.ts b/src/connectors/numeric-menu/connectNumericMenu.ts index e132732426..bf55543acd 100644 --- a/src/connectors/numeric-menu/connectNumericMenu.ts +++ b/src/connectors/numeric-menu/connectNumericMenu.ts @@ -160,7 +160,7 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( */ instantSearchInstance.sendEventToInsights({ insightsMethod: 'clickedFilters', - widgetType: 'ais.numericMenu', + widgetType: this.$$type!, eventType, payload: { eventName, diff --git a/src/connectors/range/connectRange.js b/src/connectors/range/connectRange.js index 50a0c89784..5cca726679 100644 --- a/src/connectors/range/connectRange.js +++ b/src/connectors/range/connectRange.js @@ -12,6 +12,8 @@ const withUsage = createDocumentationMessageGenerator( { name: 'range-slider', connector: true } ); +const $$type = 'ais.range'; + /** * @typedef {Object} CustomRangeWidgetOptions * @property {string} attribute Name of the attribute for faceting. @@ -164,7 +166,7 @@ export default function connectRange(renderFn, unmountFn = noop) { if (filters && filters.length > 0) { instantSearchInstance.sendEventToInsights({ insightsMethod: 'clickedFilters', - widgetType: 'ais.range', + widgetType: $$type, eventType: 'click', payload: { eventName, @@ -203,7 +205,7 @@ export default function connectRange(renderFn, unmountFn = noop) { }; return { - $$type: 'ais.range', + $$type, _getCurrentRange(stats) { const pow = Math.pow(10, precision); diff --git a/src/connectors/rating-menu/connectRatingMenu.js b/src/connectors/rating-menu/connectRatingMenu.js index 900e244308..3b749bc68b 100644 --- a/src/connectors/rating-menu/connectRatingMenu.js +++ b/src/connectors/rating-menu/connectRatingMenu.js @@ -128,7 +128,7 @@ export default function connectRatingMenu(renderFn, unmountFn = noop) { if (!isRefined) { instantSearchInstance.sendEventToInsights({ insightsMethod: 'clickedFilters', - widgetType: 'ais.ratingMenu', + widgetType: this.$$type, eventType, payload: { eventName, diff --git a/src/connectors/toggle-refinement/connectToggleRefinement.js b/src/connectors/toggle-refinement/connectToggleRefinement.js index 5bf24f8c7c..c445126f17 100644 --- a/src/connectors/toggle-refinement/connectToggleRefinement.js +++ b/src/connectors/toggle-refinement/connectToggleRefinement.js @@ -151,7 +151,7 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { if (!isRefined) { instantSearchInstance.sendEventToInsights({ insightsMethod: 'clickedFilters', - widgetType: 'ais.toggleRefinement', + widgetType: this.$$type, eventType, payload: { eventName, From 1151f6ad35f453107839b3a76cfe1536e2cf12ab Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Tue, 25 Aug 2020 16:55:39 +0200 Subject: [PATCH 08/23] remove sendEvent from connectBreadcrumb --- .../breadcrumb/connectBreadcrumb.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/connectors/breadcrumb/connectBreadcrumb.ts b/src/connectors/breadcrumb/connectBreadcrumb.ts index 29587544db..bc5c7c1e14 100644 --- a/src/connectors/breadcrumb/connectBreadcrumb.ts +++ b/src/connectors/breadcrumb/connectBreadcrumb.ts @@ -2,8 +2,6 @@ import { checkRendering, warning, createDocumentationMessageGenerator, - createSendEventForFacet, - SendEventForFacet, isEqual, noop, } from '../../lib/utils'; @@ -71,11 +69,6 @@ export type BreadcrumbRendererOptions = { * True if refinement can be applied. */ canRefine: boolean; - - /** - * Send event to insights middleware - */ - sendEvent: SendEventForFacet; }; export type BreadcrumbConnector = Connector< @@ -111,19 +104,11 @@ const connectBreadcrumb: BreadcrumbConnector = function connectBreadcrumb( } const [hierarchicalFacetName] = attributes; - let sendEvent; return { $$type: 'ais.breadcrumb', init({ createURL, helper, instantSearchInstance }) { - sendEvent = createSendEventForFacet({ - instantSearchInstance, - helper, - attribute: hierarchicalFacetName, - widgetType: this.$$type!, - }); - connectorState.createURL = facetValue => { if (!facetValue) { const breadcrumb = helper.getHierarchicalFacetBreadcrumb( @@ -152,13 +137,11 @@ const connectBreadcrumb: BreadcrumbConnector = function connectBreadcrumb( hierarchicalFacetName ); if (breadcrumb.length > 0) { - sendEvent('click', breadcrumb[0]); helper .toggleRefinement(hierarchicalFacetName, breadcrumb[0]) .search(); } } else { - sendEvent('click', facetValue); helper.toggleRefinement(hierarchicalFacetName, facetValue).search(); } }; @@ -170,7 +153,6 @@ const connectBreadcrumb: BreadcrumbConnector = function connectBreadcrumb( instantSearchInstance, items: [], refine: connectorState.refine, - sendEvent, widgetParams, }, true @@ -194,7 +176,6 @@ const connectBreadcrumb: BreadcrumbConnector = function connectBreadcrumb( instantSearchInstance, items, refine: connectorState.refine, - sendEvent, widgetParams, }, false From abcf345871f4d610d1e630da2c0913e508b86cb1 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Tue, 25 Aug 2020 17:13:34 +0200 Subject: [PATCH 09/23] moved createSendEvent to the top level in the files --- .../numeric-menu/connectNumericMenu.ts | 82 +++++++++++-------- .../rating-menu/connectRatingMenu.js | 63 ++++++++------ .../connectToggleRefinement.js | 61 ++++++++------ 3 files changed, 119 insertions(+), 87 deletions(-) diff --git a/src/connectors/numeric-menu/connectNumericMenu.ts b/src/connectors/numeric-menu/connectNumericMenu.ts index bf55543acd..f31ce528f6 100644 --- a/src/connectors/numeric-menu/connectNumericMenu.ts +++ b/src/connectors/numeric-menu/connectNumericMenu.ts @@ -100,6 +100,42 @@ export type NumericMenuConnector = Connector< NumericMenuConnectorParams >; +const $$type = 'ais.numericMenu'; + +const createSendEvent = ({ instantSearchInstance, helper, attribute }) => ( + ...args +) => { + if (args.length === 1) { + instantSearchInstance.sendEventToInsights(args[0]); + return; + } + + const [eventType, facetValue, eventName = 'Filter Applied'] = args; + if (eventType !== 'click') { + return; + } + // facetValue === "%7B%22start%22:5,%22end%22:10%7D" + const filters = convertNumericRefinementsToFilters( + getRefinedState(helper.state, attribute, facetValue), + attribute + ); + if (filters && filters.length > 0) { + /* + filters === ["price<=10", "price>=5"] + */ + instantSearchInstance.sendEventToInsights({ + insightsMethod: 'clickedFilters', + widgetType: $$type, + eventType, + payload: { + eventName, + index: helper.getIndex(), + filters, + }, + }); + } +}; + const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( renderFn, unmountFn = noop @@ -123,6 +159,7 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( type ConnectorState = { refine?: (facetValue: string) => void; createURL?: (state: SearchParameters) => (facetValue: string) => string; + sendEvent?: SendEventForFacet; }; const prepareItems = (state: SearchParameters) => @@ -133,43 +170,16 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( })); const connectorState: ConnectorState = {}; - let sendEvent; return { - $$type: 'ais.numericMenu', + $$type, init({ helper, createURL, instantSearchInstance }) { - sendEvent = (...args) => { - if (args.length === 1) { - instantSearchInstance.sendEventToInsights(args[0]); - return; - } - - const [eventType, facetValue, eventName = 'Filter Applied'] = args; - if (eventType !== 'click') { - return; - } - // facetValue === "%7B%22start%22:5,%22end%22:10%7D" - const filters = convertNumericRefinementsToFilters( - getRefinedState(helper.state, attribute, facetValue), - attribute - ); - if (filters && filters.length > 0) { - /* - filters === ["price<=10", "price>=5"] - */ - instantSearchInstance.sendEventToInsights({ - insightsMethod: 'clickedFilters', - widgetType: this.$$type!, - eventType, - payload: { - eventName, - index: helper.getIndex(), - filters, - }, - }); - } - }; + connectorState.sendEvent = createSendEvent({ + instantSearchInstance, + helper, + attribute, + }); connectorState.refine = facetValue => { const refinedState = getRefinedState( @@ -177,7 +187,7 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( attribute, facetValue ); - sendEvent('click', facetValue); + connectorState.sendEvent!('click', facetValue); helper.setState(refinedState).search(); }; @@ -190,7 +200,7 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( items: transformItems(prepareItems(helper.state)), hasNoResults: true, refine: connectorState.refine, - sendEvent, + sendEvent: connectorState.sendEvent!, instantSearchInstance, widgetParams, }, @@ -205,7 +215,7 @@ const connectNumericMenu: NumericMenuConnector = function connectNumericMenu( items: transformItems(prepareItems(state)), hasNoResults: results.nbHits === 0, refine: connectorState.refine!, - sendEvent, + sendEvent: connectorState.sendEvent!, instantSearchInstance, widgetParams, }, diff --git a/src/connectors/rating-menu/connectRatingMenu.js b/src/connectors/rating-menu/connectRatingMenu.js index 3b749bc68b..fc753720bc 100644 --- a/src/connectors/rating-menu/connectRatingMenu.js +++ b/src/connectors/rating-menu/connectRatingMenu.js @@ -10,6 +10,37 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const $$type = 'ais.ratingMenu'; + +const createSendEvent = ({ + instantSearchInstance, + helper, + refinedStar, + attribute, +}) => (...args) => { + if (args.length === 1) { + instantSearchInstance.sendEventToInsights(args[0]); + return; + } + const [eventType, facetValue, eventName = 'Filter Applied'] = args; + if (eventType !== 'click') { + return; + } + const isRefined = refinedStar === Number(facetValue); + if (!isRefined) { + instantSearchInstance.sendEventToInsights({ + insightsMethod: 'clickedFilters', + widgetType: $$type, + eventType, + payload: { + eventName, + index: helper.getIndex(), + filters: [`${attribute}>=${facetValue}`], + }, + }); + } +}; + /** * @typedef {Object} StarRatingItems * @property {string} name Name corresponding to the number of stars. @@ -107,37 +138,19 @@ export default function connectRatingMenu(renderFn, unmountFn = noop) { let sendEvent; return { - $$type: 'ais.ratingMenu', + $$type, init({ helper, createURL, instantSearchInstance }) { this._toggleRefinement = this._toggleRefinement.bind(this, helper); this._createURL = state => facetValue => createURL(state.toggleRefinement(attribute, facetValue)); - sendEvent = (...args) => { - if (args.length === 1) { - instantSearchInstance.sendEventToInsights(args[0]); - return; - } - const [eventType, facetValue, eventName = 'Filter Applied'] = args; - if (eventType !== 'click') { - return; - } - const isRefined = - this._getRefinedStar(helper.state) === Number(facetValue); - if (!isRefined) { - instantSearchInstance.sendEventToInsights({ - insightsMethod: 'clickedFilters', - widgetType: this.$$type, - eventType, - payload: { - eventName, - index: helper.getIndex(), - filters: [`${attribute}>=${facetValue}`], - }, - }); - } - }; + sendEvent = createSendEvent({ + instantSearchInstance, + helper, + refinedStar: this._getRefinedStar(helper.state), + attribute, + }); renderFn( { diff --git a/src/connectors/toggle-refinement/connectToggleRefinement.js b/src/connectors/toggle-refinement/connectToggleRefinement.js index c445126f17..6bafdc3e5b 100644 --- a/src/connectors/toggle-refinement/connectToggleRefinement.js +++ b/src/connectors/toggle-refinement/connectToggleRefinement.js @@ -13,6 +13,34 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const $$type = 'ais.toggleRefinement'; + +const createSendEvent = ({ instantSearchInstance, attribute, on, helper }) => ( + ...args +) => { + if (args.length === 1) { + instantSearchInstance.sendEventToInsights(args[0]); + return; + } + const [eventType, isRefined, eventName = 'Filter Applied'] = args; + if (eventType !== 'click' || on === undefined) { + return; + } + // Checking + if (!isRefined) { + instantSearchInstance.sendEventToInsights({ + insightsMethod: 'clickedFilters', + widgetType: $$type, + eventType, + payload: { + eventName, + index: helper.getIndex(), + filters: on.map(value => `${attribute}:${JSON.stringify(value)}`), + }, + }); + } +}; + /** * @typedef {Object} ToggleValue * @property {boolean} isRefined `true` if the toggle is on. @@ -108,7 +136,7 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { let sendEvent; return { - $$type: 'ais.toggleRefinement', + $$type, _toggleRefinement(helper, { isRefined } = {}) { // Checking @@ -138,31 +166,12 @@ export default function connectToggleRefinement(renderFn, unmountFn = noop) { }, init({ state, helper, createURL, instantSearchInstance }) { - sendEvent = (...args) => { - if (args.length === 1) { - instantSearchInstance.sendEventToInsights(args[0]); - return; - } - const [eventType, isRefined, eventName = 'Filter Applied'] = args; - if (eventType !== 'click' || on === undefined) { - return; - } - // Checking - if (!isRefined) { - instantSearchInstance.sendEventToInsights({ - insightsMethod: 'clickedFilters', - widgetType: this.$$type, - eventType, - payload: { - eventName, - index: helper.getIndex(), - filters: on.map( - value => `${attribute}:${JSON.stringify(value)}` - ), - }, - }); - } - }; + sendEvent = createSendEvent({ + instantSearchInstance, + attribute, + on, + helper, + }); this._createURL = isCurrentlyRefined => () => { const valuesToRemove = isCurrentlyRefined ? on : off; From dec506e6fd9b86ea79cbc5513a67903efb1fe3ce Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Thu, 27 Aug 2020 14:16:06 +0200 Subject: [PATCH 10/23] log insights event from storybook --- .storybook/playgrounds/default.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.storybook/playgrounds/default.ts b/.storybook/playgrounds/default.ts index a3c88565ac..8ecf9a7938 100644 --- a/.storybook/playgrounds/default.ts +++ b/.storybook/playgrounds/default.ts @@ -1,5 +1,6 @@ import instantsearch from '../../src/index'; import { panel, numericMenu, hits } from '../../src/widgets'; +import { createInsightsMiddleware } from '../../src/middlewares'; export const hitsItemTemplate = `
{ + console.log('insights onEvent', props); + }, + }); + search.EXPERIMENTAL_use(insights); } export default instantSearchPlayground; From 10879da92548bdcb674d7b15c3f632ef740e7a0a Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Thu, 27 Aug 2020 14:16:16 +0200 Subject: [PATCH 11/23] add test for connectors --- .../__tests__/connectAutocomplete-test.ts | 151 ++++++++++++++++ .../__tests__/connectGeoSearch-test.js | 115 +++++++++++++ .../__tests__/connectHierarchicalMenu-test.js | 44 +++++ .../hits/__tests__/connectHits-test.ts | 161 ++++++++++++++++++ .../__tests__/connectInfiniteHits-test.ts | 161 ++++++++++++++++++ .../menu/__tests__/connectMenu-test.js | 60 +++++++ .../__tests__/connectNumericMenu-test.ts | 58 +++++++ .../range/__tests__/connectRange-test.js | 97 ++++++++++- .../__tests__/connectRatingMenu-test.js | 61 ++++++- .../rating-menu/connectRatingMenu.js | 6 +- .../__tests__/connectRefinementList-test.js | 67 ++++++++ .../__tests__/connectToggleRefinement-test.js | 78 +++++++++ src/lib/utils/isFacetRefined.ts | 4 +- 13 files changed, 1056 insertions(+), 7 deletions(-) diff --git a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts index 0d9ff1cfa0..1acb208490 100644 --- a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts +++ b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts @@ -566,4 +566,155 @@ search.addWidgets([ ); }); }); + + describe('sendEvent', () => { + let firstIndexHits; + let secondIndexHits; + let instantSearchInstance; + let render; + beforeEach(() => { + const searchClient = createSearchClient(); + render = jest.fn(); + const makeWidget = connectAutocomplete(render); + const widget = makeWidget({ escapeHTML: false }); + + const helper = algoliasearchHelper(searchClient, '', {}); + helper.search = jest.fn(); + + const initOptions = createInitOptions({ helper }); + instantSearchInstance = initOptions.instantSearchInstance; + widget.init!(initOptions); + + firstIndexHits = [ + { + name: 'Hit 1-1', + objectID: '1-1', + __queryID: 'test-query-id', + __position: 0, + }, + ]; + secondIndexHits = [ + { + name: 'Hit 2-1', + objectID: '2-1', + __queryID: 'test-query-id', + __position: 0, + }, + { + name: 'Hit 2-2', + objectID: '2-2', + __queryID: 'test-query-id', + __position: 1, + }, + ]; + + const scopedResults = [ + { + indexId: 'indexId0', + results: new SearchResults(helper.state, [ + createSingleSearchResponse({ + index: 'indexName0', + hits: firstIndexHits, + }), + ]), + helper, + }, + { + indexId: 'indexId1', + results: new SearchResults(helper.state, [ + createSingleSearchResponse({ + index: 'indexName1', + hits: secondIndexHits, + }), + ]), + helper, + }, + ]; + + widget.render!( + createRenderOptions({ instantSearchInstance, helper, scopedResults }) + ); + }); + + it('sends view event when hits are rendered', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect( + instantSearchInstance.sendEventToInsights.mock.calls[0][0] + ).toEqual({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'indexName0', + objectIDs: ['1-1'], + }, + widgetType: 'ais.autocomplete', + }); + expect( + instantSearchInstance.sendEventToInsights.mock.calls[1][0] + ).toEqual({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'indexName1', + objectIDs: ['2-1', '2-2'], + }, + widgetType: 'ais.autocomplete', + }); + }); + + it('sends click event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); // two view events for each index by render + + const { indices } = render.mock.calls[render.mock.calls.length - 1][0]; + indices[1].sendEvent('click', secondIndexHits[0], 'Product Added'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 3 + ); + expect( + instantSearchInstance.sendEventToInsights.mock.calls[2][0] + ).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: 'indexName1', + objectIDs: ['2-1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.autocomplete', + }); + }); + + it('sends conversion event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); // two view events for each index by render + + const { indices } = render.mock.calls[render.mock.calls.length - 1][0]; + indices[0].sendEvent('conversion', firstIndexHits[0], 'Product Ordered'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 3 + ); + expect( + instantSearchInstance.sendEventToInsights.mock.calls[2][0] + ).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'indexName0', + objectIDs: ['1-1'], + queryID: 'test-query-id', + }, + widgetType: 'ais.autocomplete', + }); + }); + }); }); diff --git a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js index c7626e01ee..fdd245eef1 100644 --- a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js +++ b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js @@ -1347,4 +1347,119 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ expect(searchParametersAfter.insideBoundingBox).toBeUndefined(); }); }); + + describe('sendEvent', () => { + let render; + let instantSearchInstance; + let hits; + beforeEach(() => { + hits = [ + { + objectID: 123, + _geoloc: { lat: 10, lng: 12 }, + __position: 0, + __queryID: 'test-query-id', + }, + { + objectID: 456, + _geoloc: { lat: 12, lng: 14 }, + __position: 1, + __queryID: 'test-query-id', + }, + { + objectID: 789, + _geoloc: { lat: 14, lng: 16 }, + __position: 2, + __queryID: 'test-query-id', + }, + ]; + render = jest.fn(); + const unmount = jest.fn(); + + const customGeoSearch = connectGeoSearch(render, unmount); + const widget = customGeoSearch(); + + instantSearchInstance = createInstantSearch(); + const { mainHelper: helper } = instantSearchInstance; + + widget.init({ + state: helper.state, + instantSearchInstance, + helper, + }); + + widget.render({ + results: new SearchResults(helper.state, [ + { + hits, + }, + ]), + helper, + instantSearchInstance, + }); + }); + + it('sends view event when hits are rendered', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'indexName', + objectIDs: [123, 456, 789], + }, + widgetType: 'ais.geoSearch', + }); + }); + + it('sends click event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = render.mock.calls[render.mock.calls.length - 1][0]; + sendEvent('click', hits[0], 'Location Added'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Location Added', + index: 'indexName', + objectIDs: [123], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.geoSearch', + }); + }); + + it('sends conversion event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = render.mock.calls[render.mock.calls.length - 1][0]; + sendEvent('conversion', hits[0], 'Location Saved'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Location Saved', + index: 'indexName', + objectIDs: [123], + queryID: 'test-query-id', + }, + widgetType: 'ais.geoSearch', + }); + }); + }); }); diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js index 29f6201d9f..66b3235815 100644 --- a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js +++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js @@ -1128,4 +1128,48 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica ); }); }); + + describe('sendEvent', () => { + it('sends event when a facet is added', () => { + const rendering = jest.fn(); + const instantSearchInstance = createInstantSearch(); + const makeWidget = connectHierarchicalMenu(rendering); + const widget = makeWidget({ + attributes: ['category', 'sub_category'], + }); + + const helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters(), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + + const firstRenderingOptions = rendering.mock.calls[0][0]; + const { refine } = firstRenderingOptions; + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['category:"value"'], + index: '', + }, + widgetType: 'ais.hierarchicalMenu', + }); + }); + }); }); diff --git a/src/connectors/hits/__tests__/connectHits-test.ts b/src/connectors/hits/__tests__/connectHits-test.ts index 155dcf2510..9479c2e80e 100644 --- a/src/connectors/hits/__tests__/connectHits-test.ts +++ b/src/connectors/hits/__tests__/connectHits-test.ts @@ -550,4 +550,165 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co expect(nextState.highlightPostTag).toBe(''); }); }); + + describe('sendEvent & bindEvent', () => { + let instantSearchInstance; + let renderFn; + let hits; + beforeEach(() => { + renderFn = jest.fn(); + const makeWidget = connectHits(renderFn); + const widget = makeWidget({}); + + const helper = algoliasearchHelper(createSearchClient(), '', {}); + helper.search = jest.fn(); + + const initOptions = createInitOptions({ + helper, + state: helper.state, + }); + instantSearchInstance = initOptions.instantSearchInstance; + widget.init!(initOptions); + + hits = [ + { + objectID: '1', + fake: 'data', + __queryID: 'test-query-id', + __position: 0, + }, + { + objectID: '2', + sample: 'infos', + __queryID: 'test-query-id', + __position: 1, + }, + ]; + + const results = new SearchResults(helper.state, [ + createSingleSearchResponse({ hits }), + ]); + widget.render!( + createRenderOptions({ + results, + state: helper.state, + helper, + }) + ); + }); + + describe('sendEvent', () => { + it('sends view event when hits are rendered', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: '', + objectIDs: ['1', '2'], + }, + widgetType: 'ais.hits', + }); + }); + + it('sends click event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('click', hits[0], 'Product Added'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); + }); + + it('sends conversion event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('conversion', hits[1], 'Product Ordered'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); + }); + }); + + describe('bindEvent', () => { + it('returns a payload for click event', () => { + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('click', hits[0], 'Product Added'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); + }); + + it('returns a payload for conversion event', () => { + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('conversion', hits[1], 'Product Ordered'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); + }); + }); + }); }); diff --git a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts index 0644e8f5a5..9f8583560d 100644 --- a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts +++ b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts @@ -978,4 +978,165 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi expect(actual.page).toEqual(2); }); }); + + describe('sendEvent & bindEvent', () => { + let instantSearchInstance; + let renderFn; + let hits; + beforeEach(() => { + renderFn = jest.fn(); + const makeWidget = connectInfiniteHits(renderFn); + const widget = makeWidget({}); + + const helper = algoliasearchHelper(createSearchClient(), '', {}); + helper.search = jest.fn(); + + const initOptions = createInitOptions({ + helper, + state: helper.state, + }); + instantSearchInstance = initOptions.instantSearchInstance; + widget.init!(initOptions); + + hits = [ + { + objectID: '1', + fake: 'data', + __queryID: 'test-query-id', + __position: 0, + }, + { + objectID: '2', + sample: 'infos', + __queryID: 'test-query-id', + __position: 1, + }, + ]; + + const results = new SearchResults(helper.state, [ + createSingleSearchResponse({ hits }), + ]); + widget.render!( + createRenderOptions({ + results, + state: helper.state, + helper, + }) + ); + }); + + describe('sendEvent', () => { + it('sends view event when hits are rendered', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: '', + objectIDs: ['1', '2'], + }, + widgetType: 'ais.infiniteHits', + }); + }); + + it('sends click event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('click', hits[0], 'Product Added'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); + }); + + it('sends conversion event', () => { + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('conversion', hits[1], 'Product Ordered'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); + }); + }); + + describe('bindEvent', () => { + it('returns a payload for click event', () => { + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('click', hits[0], 'Product Added'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); + }); + + it('returns a payload for conversion event', () => { + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('conversion', hits[1], 'Product Ordered'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); + }); + }); + }); }); diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.js index 977fafd26b..5f5c038bf2 100644 --- a/src/connectors/menu/__tests__/connectMenu-test.js +++ b/src/connectors/menu/__tests__/connectMenu-test.js @@ -1087,4 +1087,64 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co expect(newState).toEqual(new SearchParameters()); }); }); + + describe('sendEvent', () => { + let instantSearchInstance; + let helper; + beforeEach(() => { + const widget = makeWidget({ + attribute: 'category', + }); + instantSearchInstance = createInstantSearch(); + helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters(), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + }); + + it('sends event when a facet is refined', () => { + const firstRenderingOptions = rendering.mock.calls[0][0]; + const { refine } = firstRenderingOptions; + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['category:"value"'], + index: '', + }, + widgetType: 'ais.menu', + }); + }); + + it('does not send event when a facet is removed', () => { + const firstRenderingOptions = rendering.mock.calls[0][0]; + const { refine } = firstRenderingOptions; + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(helper.hasRefinements('category')).toBe(true); + + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // still the same + }); + }); }); diff --git a/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts index 8e6737bf0e..647d19ad34 100644 --- a/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts +++ b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts @@ -1017,4 +1017,62 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/numeric-men ); }); }); + + describe('sendEvent', () => { + it('sends event when a facet is added', () => { + const rendering = jest.fn(); + const makeWidget = connectNumericMenu(rendering); + const widget = makeWidget({ + attribute: 'numerics', + items: [ + { label: 'below 10', end: 10 }, + { label: '10 - 20', start: 10, end: 20 }, + { label: 'more than 20', start: 20 }, + { label: '42', start: 42, end: 42 }, + { label: 'void' }, + ], + }); + + const helper = jsHelper(createSearchClient(), ''); + helper.search = jest.fn(); + const initOptions = createInitOptions({ + helper, + state: helper.state, + }); + const { instantSearchInstance } = initOptions; + widget.init!(initOptions); + + const firstRenderingOptions = rendering.mock.calls[0][0]; + const { refine, items } = firstRenderingOptions; + refine(items[0].value); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['numerics<=10'], + index: '', + }, + widgetType: 'ais.numericMenu', + }); + + refine(items[1].value); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['numerics<=20', 'numerics>=10'], + index: '', + }, + widgetType: 'ais.numericMenu', + }); + }); + }); }); diff --git a/src/connectors/range/__tests__/connectRange-test.js b/src/connectors/range/__tests__/connectRange-test.js index 9635beafb5..d51088a207 100644 --- a/src/connectors/range/__tests__/connectRange-test.js +++ b/src/connectors/range/__tests__/connectRange-test.js @@ -1415,7 +1415,7 @@ describe('getWidgetSearchParameters', () => { }); const attribute = 'price'; - const rendering = () => {}; + let rendering = () => {}; it('expect to return default configuration', () => { const widget = connectRange(rendering)({ @@ -1532,4 +1532,99 @@ describe('getWidgetSearchParameters', () => { expect(actual).toEqual(expectation); }); + + describe('sendEvent', () => { + it('sends event when a facet is added at each step', () => { + rendering = jest.fn(); + const makeWidget = connectRange(rendering); + const widget = makeWidget({ + attribute, + }); + + const instantSearchInstance = createInstantSearch(); + const helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters(), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + + { + // first rendering + const renderOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = renderOptions; + refine([10, 30]); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['price>=10', 'price<=30'], + index: '', + }, + widgetType: 'ais.range', + }); + } + + widget.render({ + results: new SearchResults(helper.state, [ + { + hits: [{ test: 'oneTime' }], + facets: { price: { 10: 1, 20: 1, 30: 1 } }, + // eslint-disable-next-line @typescript-eslint/camelcase + facets_stats: { + price: { + avg: 20, + max: 30, + min: 10, + sum: 60, + }, + }, + nbHits: 1, + nbPages: 1, + page: 0, + }, + {}, + ]), + state: helper.state, + helper, + createURL: () => '#', + instantSearchInstance, + }); + + { + // Second rendering + const renderOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = renderOptions; + refine([23, 27]); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['price>=23', 'price<=27'], + index: '', + }, + widgetType: 'ais.range', + }); + } + }); + }); }); diff --git a/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js index 2b47ee2532..fe040684a5 100644 --- a/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js +++ b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js @@ -9,6 +9,7 @@ describe('connectRatingMenu', () => { const getInitializedWidget = (config = {}, unmount) => { const rendering = jest.fn(); const makeWidget = connectRatingMenu(rendering, unmount); + const instantSearchInstance = createInstantSearch(); const attribute = 'grade'; const widget = makeWidget({ @@ -27,12 +28,12 @@ describe('connectRatingMenu', () => { helper, state: helper.state, createURL: () => '#', - instantSearchInstance: createInstantSearch(), + instantSearchInstance, }); const { refine } = rendering.mock.calls[0][0]; - return { widget, helper, refine, rendering }; + return { widget, helper, refine, rendering, instantSearchInstance }; }; describe('Usage', () => { @@ -533,4 +534,60 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/rating-menu ); }); }); + + describe('sendEvent', () => { + it('sends event when a facet is added', () => { + const attribute = 'swag'; + const { refine, instantSearchInstance } = getInitializedWidget({ + attribute, + }); + + refine('3'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['swag>=3'], + index: '', + }, + widgetType: 'ais.ratingMenu', + }); + }); + + it('does not send event when a facet is removed', () => { + const attribute = 'swag'; + const { refine, instantSearchInstance } = getInitializedWidget({ + attribute, + }); + + refine('3'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + + refine('3'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // still the same + + refine('4'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 2 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['swag>=4'], + index: '', + }, + widgetType: 'ais.ratingMenu', + }); + }); + }); }); diff --git a/src/connectors/rating-menu/connectRatingMenu.js b/src/connectors/rating-menu/connectRatingMenu.js index fc753720bc..96e66be139 100644 --- a/src/connectors/rating-menu/connectRatingMenu.js +++ b/src/connectors/rating-menu/connectRatingMenu.js @@ -15,7 +15,7 @@ const $$type = 'ais.ratingMenu'; const createSendEvent = ({ instantSearchInstance, helper, - refinedStar, + getRefinedStar, attribute, }) => (...args) => { if (args.length === 1) { @@ -26,7 +26,7 @@ const createSendEvent = ({ if (eventType !== 'click') { return; } - const isRefined = refinedStar === Number(facetValue); + const isRefined = getRefinedStar() === Number(facetValue); if (!isRefined) { instantSearchInstance.sendEventToInsights({ insightsMethod: 'clickedFilters', @@ -148,7 +148,7 @@ export default function connectRatingMenu(renderFn, unmountFn = noop) { sendEvent = createSendEvent({ instantSearchInstance, helper, - refinedStar: this._getRefinedStar(helper.state), + getRefinedStar: () => this._getRefinedStar(helper.state), attribute, }); diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js index f09d1c2fff..612a08dab1 100644 --- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js +++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js @@ -2400,4 +2400,71 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- }); }); }); + + describe('sendEvent', () => { + let makeWidget; + let rendering; + let instantSearchInstance; + + beforeEach(() => { + const factoryResult = createWidgetFactory(); + makeWidget = factoryResult.makeWidget; + rendering = factoryResult.rendering; + instantSearchInstance = createInstantSearch(); + const widget = makeWidget({ + attribute: 'category', + }); + + const helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters({}), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + }); + + it('sends event when a facet is added', () => { + const firstRenderingOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = firstRenderingOptions; + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['category:"value"'], + index: '', + }, + widgetType: 'ais.refinementList', + }); + }); + + it('does not send event when a facet is removed', () => { + const firstRenderingOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = firstRenderingOptions; + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + + refine('value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // still the same + }); + }); }); diff --git a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js index 3200302ad6..d8545e16ac 100644 --- a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js +++ b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js @@ -1091,4 +1091,82 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi }); }); }); + + describe('sendEvent', () => { + let rendering; + let helper; + let instantSearchInstance; + let widget; + + beforeEach(() => { + rendering = jest.fn(); + instantSearchInstance = createInstantSearch(); + const makeWidget = connectToggleRefinement(rendering); + + const attribute = 'isShippingFree'; + widget = makeWidget({ + attribute, + }); + + helper = jsHelper( + {}, + '', + widget.getWidgetSearchParameters(new SearchParameters({}), { + uiState: {}, + }) + ); + helper.search = jest.fn(); + }); + + it('sends event when a facet is added', () => { + // first rendering + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + + const renderOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = renderOptions; + refine({ isRefined: false }); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['isShippingFree:true'], + index: '', + }, + widgetType: 'ais.toggleRefinement', + }); + }); + + it('does not send event when a facet is removed', () => { + // first rendering + widget.init({ + helper, + state: helper.state, + createURL: () => '#', + instantSearchInstance, + }); + + const renderOptions = + rendering.mock.calls[rendering.mock.calls.length - 1][0]; + const { refine } = renderOptions; + refine({ isRefined: false }); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + + refine({ isRefined: true }); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); // still the same + }); + }); }); diff --git a/src/lib/utils/isFacetRefined.ts b/src/lib/utils/isFacetRefined.ts index fbea8eef9f..6947a844a5 100644 --- a/src/lib/utils/isFacetRefined.ts +++ b/src/lib/utils/isFacetRefined.ts @@ -5,7 +5,9 @@ export default function isFacetRefined( facet: string, value: string ) { - if (helper.state.isConjunctiveFacet(facet)) { + if (helper.state.isHierarchicalFacet(facet)) { + return helper.state.isHierarchicalFacetRefined(facet, value); + } else if (helper.state.isConjunctiveFacet(facet)) { return helper.state.isFacetRefined(facet, value); } else { return helper.state.isDisjunctiveFacetRefined(facet, value); From 59f29d2991b876ae454a87b82d84ceddef7586a6 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Thu, 27 Aug 2020 15:39:17 +0200 Subject: [PATCH 12/23] add tests for createSendEvent helpers --- .../__tests__/createSendEventForFacet-test.ts | 104 ++++++++ .../__tests__/createSendEventForHits-test.ts | 222 ++++++++++++++++++ src/lib/utils/createSendEventForFacet.ts | 11 +- src/lib/utils/createSendEventForHits.ts | 2 +- 4 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 src/lib/utils/__tests__/createSendEventForFacet-test.ts create mode 100644 src/lib/utils/__tests__/createSendEventForHits-test.ts diff --git a/src/lib/utils/__tests__/createSendEventForFacet-test.ts b/src/lib/utils/__tests__/createSendEventForFacet-test.ts new file mode 100644 index 0000000000..be960f978b --- /dev/null +++ b/src/lib/utils/__tests__/createSendEventForFacet-test.ts @@ -0,0 +1,104 @@ +import algoliasearchHelper from 'algoliasearch-helper'; +import { createSendEventForFacet } from '../createSendEventForFacet'; +import { InstantSearch, SearchClient } from '../../../types'; + +jest.mock('../isFacetRefined', () => jest.fn()); + +import isFacetRefined from '../isFacetRefined'; + +const instantSearchInstance = {} as InstantSearch; +let sendEvent; +let helper; +beforeEach(() => { + instantSearchInstance.sendEventToInsights = jest.fn(); + helper = algoliasearchHelper({} as SearchClient, '', {}); + (isFacetRefined as jest.Mock).mockImplementation(() => false); + sendEvent = createSendEventForFacet({ + instantSearchInstance, + helper, + attribute: 'category', + widgetType: 'ais.customWidget', + }); +}); + +describe('createSendEventForFacet', () => { + describe('Usage', () => { + it('throws when facetValue is missing', () => { + expect(() => { + sendEvent('click'); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass two arguments like: + sendEvent('click', facetValue); + +If you want to send a custom payload, you can pass one object: sendEvent(customPayload); +" +`); + }); + + it('throws with unknown eventType', () => { + expect(() => { + sendEvent('my custom event type'); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass two arguments like: + sendEvent('click', facetValue); + +If you want to send a custom payload, you can pass one object: sendEvent(customPayload); +" +`); + }); + + it('throws when eventType is not click', () => { + expect(() => { + sendEvent('custom event type'); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass two arguments like: + sendEvent('click', facetValue); + +If you want to send a custom payload, you can pass one object: sendEvent(customPayload); +" +`); + }); + + it('does not send event when a facet is removed', () => { + (isFacetRefined as jest.Mock).mockImplementation(() => true); + sendEvent('click', 'value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 0 + ); + }); + + it('sends with default eventName', () => { + sendEvent('click', 'value'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Filter Applied', + filters: ['category:"value"'], + index: '', + }, + widgetType: 'ais.customWidget', + }); + }); + + it('sends with custom eventName', () => { + sendEvent('click', 'value', 'Category Clicked'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( + 1 + ); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedFilters', + payload: { + eventName: 'Category Clicked', + filters: ['category:"value"'], + index: '', + }, + widgetType: 'ais.customWidget', + }); + }); + }); +}); diff --git a/src/lib/utils/__tests__/createSendEventForHits-test.ts b/src/lib/utils/__tests__/createSendEventForHits-test.ts new file mode 100644 index 0000000000..cb35296789 --- /dev/null +++ b/src/lib/utils/__tests__/createSendEventForHits-test.ts @@ -0,0 +1,222 @@ +import { + createSendEventForHits, + createBindEventForHits, +} from '../createSendEventForHits'; +import { InstantSearch } from '../../../types'; + +const instantSearchInstance = {} as InstantSearch; +const index = 'testIndex'; +const widgetType = 'ais.testWidget'; +const hits = [ + { + objectID: 'obj0', + __position: 0, + __queryID: 'test-query-id', + }, + { + objectID: 'obj1', + __position: 1, + __queryID: 'test-query-id', + }, +]; + +beforeEach(() => { + instantSearchInstance.sendEventToInsights = jest.fn(); +}); + +describe('createSendEventForHits', () => { + let sendEvent; + + beforeEach(() => { + sendEvent = createSendEventForHits({ + instantSearchInstance, + index, + widgetType, + }); + }); + + describe('Usage', () => { + it('throws when hit is missing', () => { + expect(() => { + sendEvent('click'); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass hit or hits as the second argument like: + sendEvent(eventType, hit); + " +`); + }); + + it('throws with unknown eventType', () => { + expect(() => { + sendEvent('my custom event type'); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass hit or hits as the second argument like: + sendEvent(eventType, hit); + " +`); + }); + + it('throw when eventName is missing for click or conversion event', () => { + expect(() => { + sendEvent('click', {}); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass eventName as the third argument for 'click' or 'conversion' events like: + sendEvent('click', hit, 'Product Purchased'); + + To learn more about event naming: https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/in-depth/clicks-conversions-best-practices/ + " +`); + + expect(() => { + sendEvent('conversion', {}); + }).toThrowErrorMatchingInlineSnapshot(` +"You need to pass eventName as the third argument for 'click' or 'conversion' events like: + sendEvent('click', hit, 'Product Purchased'); + + To learn more about event naming: https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/in-depth/clicks-conversions-best-practices/ + " +`); + }); + }); + + it('sends view event with default eventName', () => { + sendEvent('view', hits[0]); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'testIndex', + objectIDs: ['obj0'], + }, + widgetType: 'ais.testWidget', + }); + }); + + it('sends view event with custom eventName', () => { + sendEvent('view', hits[0], 'Products Displayed'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Products Displayed', + index: 'testIndex', + objectIDs: ['obj0'], + }, + widgetType: 'ais.testWidget', + }); + }); + + it('sends view event with multiple hits', () => { + sendEvent('view', hits); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'testIndex', + objectIDs: ['obj0', 'obj1'], + }, + widgetType: 'ais.testWidget', + }); + }); + + it('sends click event', () => { + sendEvent('click', hits[0], 'Product Clicked'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Clicked', + index: 'testIndex', + objectIDs: ['obj0'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.testWidget', + }); + }); + + it('sends conversion event', () => { + sendEvent('conversion', hits[0], 'Product Ordered'); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'testIndex', + objectIDs: ['obj0'], + queryID: 'test-query-id', + }, + widgetType: 'ais.testWidget', + }); + }); + + it('sends custom event', () => { + sendEvent({ + hello: 'world', + custom: 'event', + }); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); + expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ + hello: 'world', + custom: 'event', + }); + }); +}); + +describe('createBindEventForHits', () => { + let bindEvent; + + beforeEach(() => { + bindEvent = createBindEventForHits({ + index, + widgetType, + }); + }); + + function parsePayload(payload) { + expect(payload.startsWith('data-insights-event=')).toBe(true); + return JSON.parse(atob(payload.substr('data-insights-event='.length))); + } + + it('returns a payload for click event', () => { + const parsedPayload = parsePayload( + bindEvent('click', hits[0], 'Product Clicked') + ); + expect(parsedPayload).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Clicked', + index: 'testIndex', + objectIDs: ['obj0'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.testWidget', + }); + }); + + it('returns a payload for conversion event', () => { + const parsedPayload = parsePayload( + bindEvent('conversion', hits[0], 'Product Ordered') + ); + expect(parsedPayload).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'testIndex', + objectIDs: ['obj0'], + queryID: 'test-query-id', + }, + widgetType: 'ais.testWidget', + }); + }); +}); diff --git a/src/lib/utils/createSendEventForFacet.ts b/src/lib/utils/createSendEventForFacet.ts index 0e353f66da..57d60b0836 100644 --- a/src/lib/utils/createSendEventForFacet.ts +++ b/src/lib/utils/createSendEventForFacet.ts @@ -25,7 +25,12 @@ export function createSendEventForFacet({ }): SendEventForFacet { const sendEventForFacet: SendEventForFacet = (...args) => { const [eventType, facetValue, eventName = 'Filter Applied'] = args; - if (eventType === 'click' && (args.length === 2 || args.length === 3)) { + if (args.length === 1 && typeof args[0] === 'object') { + instantSearchInstance.sendEventToInsights(args[0]); + } else if ( + eventType === 'click' && + (args.length === 2 || args.length === 3) + ) { if (!isFacetRefined(helper, attribute, facetValue)) { // send event only when the facet is being checked "ON" instantSearchInstance.sendEventToInsights({ @@ -39,12 +44,10 @@ export function createSendEventForFacet({ }, }); } - } else if (args.length === 1) { - instantSearchInstance.sendEventToInsights(args[0]); } else if (__DEV__) { throw new Error( `You need to pass two arguments like: -sendEvent('click', facetValue); + sendEvent('click', facetValue); If you want to send a custom payload, you can pass one object: sendEvent(customPayload); ` diff --git a/src/lib/utils/createSendEventForHits.ts b/src/lib/utils/createSendEventForHits.ts index 19e4c6aa91..a7541aa572 100644 --- a/src/lib/utils/createSendEventForHits.ts +++ b/src/lib/utils/createSendEventForHits.ts @@ -30,7 +30,7 @@ const buildPayload: BuildPayload = ({ methodName, args, }) => { - if (args.length === 1) { + if (args.length === 1 && typeof args[0] === 'object') { return args[0]; } const eventType: string = args[0]; From 5d7428e9e8d3880b267b58b014200a0f705b487f Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Thu, 27 Aug 2020 17:01:17 +0200 Subject: [PATCH 13/23] add integration tests for hits and infinite-hits widgets with bindEvent in templates --- src/types/widget.ts | 5 + .../hits/__tests__/hits-integration-test.ts | 149 +++++++++ src/widgets/hits/hits.tsx | 3 +- .../infinite-hits-integration-test.ts | 295 ++++++++++++------ src/widgets/infinite-hits/infinite-hits.tsx | 3 +- 5 files changed, 366 insertions(+), 89 deletions(-) create mode 100644 src/widgets/hits/__tests__/hits-integration-test.ts diff --git a/src/types/widget.ts b/src/types/widget.ts index b7ff1ba02d..164b75e05c 100644 --- a/src/types/widget.ts +++ b/src/types/widget.ts @@ -6,6 +6,7 @@ import { PlainSearchParameters, } from 'algoliasearch-helper'; import { InstantSearch } from './instantsearch'; +import { BindEventForHits } from '../lib/utils'; export type InitOptions = { instantSearchInstance: InstantSearch; @@ -205,3 +206,7 @@ export type WidgetFactory = ( export type Template = | string | ((data: TTemplateData) => string); + +export type TemplateWithBindEvent = + | string + | ((data: TTemplateData, bindEvent: BindEventForHits) => string); diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts new file mode 100644 index 0000000000..a2e9b4b901 --- /dev/null +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -0,0 +1,149 @@ +import { getByText, fireEvent } from '@testing-library/dom'; + +import instantsearch from '../../../index.es'; +import { hits, configure } from '../../'; +import { createInsightsMiddleware } from '../../../middlewares'; +import { createSingleSearchResponse } from '../../../../test/mock/createAPIResponse'; + +describe('hits', () => { + let search; + let searchClient; + let container; + const hitsPerPage = 2; + const page = 0; + + beforeEach(() => { + searchClient = { + search: jest.fn(requests => + Promise.resolve({ + results: requests.map(() => + createSingleSearchResponse({ + hits: Array(hitsPerPage) + .fill(undefined) + .map((_, index) => ({ + title: `title ${page * hitsPerPage + index + 1}`, + objectID: `object-id${index}`, + })), + }) + ), + }) + ), + }; + search = instantsearch({ + indexName: 'instant_search', + searchClient, + }); + + container = document.createElement('div'); + }); + + describe('sendEvent', () => { + let onEvent; + beforeEach(() => { + onEvent = jest.fn(); + const insights = createInsightsMiddleware({ + insightsClient: null, + onEvent, + }); + search.EXPERIMENTAL_use(insights); + + search.addWidgets([ + configure({ + hitsPerPage, + }), + ]); + }); + + it('sends view event when hits are rendered', done => { + search.addWidgets([ + hits({ + container, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.hits', + }); + done(); + }); + }); + + it('sends click event', done => { + search.addWidgets([ + hits({ + container, + templates: { + item: (item, bindEvent) => ` + + `, + }, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 1')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Item Clicked', + index: 'instant_search', + objectIDs: ['object-id0'], + positions: [1], + }, + widgetType: 'ais.hits', + }); + done(); + }); + }); + + it('sends conversion event', done => { + search.addWidgets([ + hits({ + container, + templates: { + item: (item, bindEvent) => ` + + `, + }, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 2')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'instant_search', + objectIDs: ['object-id1'], + }, + widgetType: 'ais.hits', + }); + done(); + }); + }); + }); +}); diff --git a/src/widgets/hits/hits.tsx b/src/widgets/hits/hits.tsx index 3f0a7962ce..454dea9b4d 100644 --- a/src/widgets/hits/hits.tsx +++ b/src/widgets/hits/hits.tsx @@ -17,6 +17,7 @@ import { component } from '../../lib/suit'; import { withInsights, withInsightsListener } from '../../lib/insights'; import { Template, + TemplateWithBindEvent, Hit, WidgetFactory, Renderer, @@ -98,7 +99,7 @@ export type HitsTemplates = { * * @default '' */ - item?: Template< + item?: TemplateWithBindEvent< Hit & { __hitIndex: number; } diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 39bd3ff7ab..6301a56b62 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -2,6 +2,7 @@ import { getByText, waitFor, fireEvent } from '@testing-library/dom'; import instantsearch from '../../../index.es'; import { infiniteHits, configure } from '../../'; +import { createInsightsMiddleware } from '../../../middlewares'; function createSingleSearchResponse({ params: { hitsPerPage, page } }) { return { @@ -9,6 +10,7 @@ function createSingleSearchResponse({ params: { hitsPerPage, page } }) { .fill(undefined) .map((_, index) => ({ title: `title ${page * hitsPerPage + index + 1}`, + objectID: `object-id${index}`, })), page, hitsPerPage, @@ -20,10 +22,6 @@ describe('infiniteHits', () => { let searchClient; let container; - let cachedState: any; - let cachedHits: any; - let customCache; - beforeEach(() => { searchClient = { search: jest.fn(requests => @@ -38,102 +36,225 @@ describe('infiniteHits', () => { }); container = document.createElement('div'); - - const getStateWithoutPage = state => { - const { page, ...rest } = state || {}; - return rest; - }; - const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); - cachedState = undefined; - cachedHits = undefined; - customCache = { - read: jest.fn(({ state }) => { - return isEqual(cachedState, getStateWithoutPage(state)) - ? cachedHits - : null; - }), - write: jest.fn(({ state, hits }) => { - cachedState = getStateWithoutPage(state); - cachedHits = hits; - }), - }; }); - it('calls read & write methods of custom cache', async () => { - search.addWidgets([ - configure({ - hitsPerPage: 2, - }), - infiniteHits({ - container, - cache: customCache, - }), - ]); - search.start(); - - await waitFor(() => { - const numberOfHits = container.querySelectorAll('.ais-InfiniteHits-item') - .length; - expect(numberOfHits).toEqual(2); + describe('cache', () => { + let cachedState: any; + let cachedHits: any; + let customCache; + + beforeEach(() => { + const getStateWithoutPage = state => { + const { page, ...rest } = state || {}; + return rest; + }; + const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); + cachedState = undefined; + cachedHits = undefined; + customCache = { + read: jest.fn(({ state }) => { + return isEqual(cachedState, getStateWithoutPage(state)) + ? cachedHits + : null; + }), + write: jest.fn(({ state, hits }) => { + cachedState = getStateWithoutPage(state); + cachedHits = hits; + }), + }; }); - fireEvent.click(getByText(container, 'Show more results')); - await waitFor(() => { - const numberOfHits = container.querySelectorAll('.ais-InfiniteHits-item') - .length; - expect(numberOfHits).toEqual(4); + it('calls read & write methods of custom cache', async () => { + search.addWidgets([ + configure({ + hitsPerPage: 2, + }), + infiniteHits({ + container, + cache: customCache, + }), + ]); + search.start(); + + await waitFor(() => { + const numberOfHits = container.querySelectorAll( + '.ais-InfiniteHits-item' + ).length; + expect(numberOfHits).toEqual(2); + }); + + fireEvent.click(getByText(container, 'Show more results')); + await waitFor(() => { + const numberOfHits = container.querySelectorAll( + '.ais-InfiniteHits-item' + ).length; + expect(numberOfHits).toEqual(4); + }); + + expect(customCache.read).toHaveBeenCalledTimes(2); // init & render + expect(customCache.write).toHaveBeenCalledTimes(2); // page #0, page #1 }); - expect(customCache.read).toHaveBeenCalledTimes(2); // init & render - expect(customCache.write).toHaveBeenCalledTimes(2); // page #0, page #1 + it('displays all the hits from cache', async () => { + // flow #1 - load page #0 & #1 to fill the cache + search.addWidgets([ + configure({ + hitsPerPage: 2, + }), + infiniteHits({ + container, + cache: customCache, + }), + ]); + search.start(); + + await waitFor(() => { + const numberOfHits = container.querySelectorAll( + '.ais-InfiniteHits-item' + ).length; + expect(numberOfHits).toEqual(2); + }); + + fireEvent.click(getByText(container, 'Show more results')); + await waitFor(() => { + const numberOfHits = container.querySelectorAll( + '.ais-InfiniteHits-item' + ).length; + expect(numberOfHits).toEqual(4); + }); + + // flow #2 - new InstantSearch instance to leverage the cache + const search2 = instantsearch({ + indexName: 'instant_search', + searchClient, + }); + const container2 = document.createElement('div'); + search2.addWidgets([ + configure({ + hitsPerPage: 2, + }), + infiniteHits({ + container: container2, + cache: customCache, // same cache object + }), + ]); + search2.start(); + await waitFor(() => { + const numberOfHits = container.querySelectorAll( + '.ais-InfiniteHits-item' + ).length; + expect(numberOfHits).toEqual(4); // it loads two pages initially + }); + }); }); - it('displays all the hits from cache', async () => { - // flow #1 - load page #0 & #1 to fill the cache - search.addWidgets([ - configure({ - hitsPerPage: 2, - }), - infiniteHits({ - container, - cache: customCache, - }), - ]); - search.start(); - - await waitFor(() => { - const numberOfHits = container.querySelectorAll('.ais-InfiniteHits-item') - .length; - expect(numberOfHits).toEqual(2); + describe('sendEvent', () => { + let onEvent; + beforeEach(() => { + onEvent = jest.fn(); + const insights = createInsightsMiddleware({ + insightsClient: null, + onEvent, + }); + search.EXPERIMENTAL_use(insights); + + search.addWidgets([ + configure({ + hitsPerPage: 2, + }), + ]); }); - fireEvent.click(getByText(container, 'Show more results')); - await waitFor(() => { - const numberOfHits = container.querySelectorAll('.ais-InfiniteHits-item') - .length; - expect(numberOfHits).toEqual(4); + it('sends view event when hits are rendered', done => { + search.addWidgets([ + infiniteHits({ + container, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.infiniteHits', + }); + done(); + }); }); - // flow #2 - new InstantSearch instance to leverage the cache - const search2 = instantsearch({ - indexName: 'instant_search', - searchClient, + it('sends click event', done => { + search.addWidgets([ + infiniteHits({ + container, + templates: { + item: (item, bindEvent) => ` + + `, + }, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 1')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Item Clicked', + index: 'instant_search', + objectIDs: ['object-id0'], + positions: [1], + }, + widgetType: 'ais.infiniteHits', + }); + done(); + }); }); - const container2 = document.createElement('div'); - search2.addWidgets([ - configure({ - hitsPerPage: 2, - }), - infiniteHits({ - container: container2, - cache: customCache, // same cache object - }), - ]); - search2.start(); - await waitFor(() => { - const numberOfHits = container.querySelectorAll('.ais-InfiniteHits-item') - .length; - expect(numberOfHits).toEqual(4); // it loads two pages initially + + it('sends conversion event', done => { + search.addWidgets([ + infiniteHits({ + container, + templates: { + item: (item, bindEvent) => ` + + `, + }, + }), + ]); + search.start(); + process.nextTick(() => { + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 2')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'instant_search', + objectIDs: ['object-id1'], + }, + widgetType: 'ais.infiniteHits', + }); + done(); + }); }); }); }); diff --git a/src/widgets/infinite-hits/infinite-hits.tsx b/src/widgets/infinite-hits/infinite-hits.tsx index 123f79a16b..92c00b5268 100644 --- a/src/widgets/infinite-hits/infinite-hits.tsx +++ b/src/widgets/infinite-hits/infinite-hits.tsx @@ -19,6 +19,7 @@ import { withInsights, withInsightsListener } from '../../lib/insights'; import { WidgetFactory, Template, + TemplateWithBindEvent, Hit, InsightsClientWrapper, Renderer, @@ -93,7 +94,7 @@ export type InfiniteHitsTemplates = { /** * The template to use for each result. */ - item?: Template; + item?: TemplateWithBindEvent; }; export type InfiniteHitsWidgetParams = { From 4c41441d628e30c130e66c71bd055b3824891d44 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Thu, 27 Aug 2020 18:08:34 +0200 Subject: [PATCH 14/23] update bundlesize --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ef0c65f016..b32cb13699 100644 --- a/package.json +++ b/package.json @@ -142,11 +142,11 @@ "bundlesize": [ { "path": "./dist/instantsearch.production.min.js", - "maxSize": "66 kB" + "maxSize": "64.50 kB" }, { "path": "./dist/instantsearch.development.js", - "maxSize": "160 kB" + "maxSize": "150.40 kB" } ] } From 674e4a4397a6ced105db6e46f1d94aaa15f72eb8 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Fri, 28 Aug 2020 16:54:49 +0200 Subject: [PATCH 15/23] Update src/connectors/hits/__tests__/connectHits-test.ts Co-authored-by: Haroen Viaene --- src/connectors/hits/__tests__/connectHits-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connectors/hits/__tests__/connectHits-test.ts b/src/connectors/hits/__tests__/connectHits-test.ts index 9479c2e80e..4b25fc96ba 100644 --- a/src/connectors/hits/__tests__/connectHits-test.ts +++ b/src/connectors/hits/__tests__/connectHits-test.ts @@ -551,7 +551,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); }); - describe('sendEvent & bindEvent', () => { + describe('insights', () => { let instantSearchInstance; let renderFn; let hits; From 669501559bff1fda2a340c02658b87fe574053a9 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Fri, 28 Aug 2020 17:58:13 +0200 Subject: [PATCH 16/23] use factory function instead of globally exposed variables --- .../__tests__/connectAutocomplete-test.ts | 34 ++- .../__tests__/connectGeoSearch-test.js | 22 +- .../hits/__tests__/connectHits-test.ts | 28 ++- .../__tests__/connectInfiniteHits-test.ts | 28 ++- .../menu/__tests__/connectMenu-test.js | 14 +- .../__tests__/connectRefinementList-test.js | 21 +- .../__tests__/connectToggleRefinement-test.js | 32 +-- .../__tests__/createSendEventForFacet-test.ts | 28 ++- .../__tests__/createSendEventForHits-test.ts | 87 ++++---- src/middlewares/__tests__/insights.ts | 199 ++++++++++++------ .../hits/__tests__/hits-integration-test.ts | 90 ++++---- .../infinite-hits-integration-test.ts | 61 +++--- 12 files changed, 404 insertions(+), 240 deletions(-) diff --git a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts index 1acb208490..7d6dcc7471 100644 --- a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts +++ b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts @@ -568,13 +568,9 @@ search.addWidgets([ }); describe('sendEvent', () => { - let firstIndexHits; - let secondIndexHits; - let instantSearchInstance; - let render; - beforeEach(() => { + const createRenderedWidget = () => { const searchClient = createSearchClient(); - render = jest.fn(); + const render = jest.fn(); const makeWidget = connectAutocomplete(render); const widget = makeWidget({ escapeHTML: false }); @@ -582,10 +578,10 @@ search.addWidgets([ helper.search = jest.fn(); const initOptions = createInitOptions({ helper }); - instantSearchInstance = initOptions.instantSearchInstance; + const instantSearchInstance = initOptions.instantSearchInstance; widget.init!(initOptions); - firstIndexHits = [ + const firstIndexHits = [ { name: 'Hit 1-1', objectID: '1-1', @@ -593,7 +589,7 @@ search.addWidgets([ __position: 0, }, ]; - secondIndexHits = [ + const secondIndexHits = [ { name: 'Hit 2-1', objectID: '2-1', @@ -634,9 +630,17 @@ search.addWidgets([ widget.render!( createRenderOptions({ instantSearchInstance, helper, scopedResults }) ); - }); + + return { + instantSearchInstance, + render, + firstIndexHits, + secondIndexHits, + }; + }; it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 2 ); @@ -667,6 +671,11 @@ search.addWidgets([ }); it('sends click event', () => { + const { + instantSearchInstance, + render, + secondIndexHits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 2 ); // two view events for each index by render @@ -693,6 +702,11 @@ search.addWidgets([ }); it('sends conversion event', () => { + const { + instantSearchInstance, + render, + firstIndexHits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 2 ); // two view events for each index by render diff --git a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js index fdd245eef1..3e006f976f 100644 --- a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js +++ b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js @@ -1349,11 +1349,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); describe('sendEvent', () => { - let render; - let instantSearchInstance; - let hits; - beforeEach(() => { - hits = [ + const createRenderedWidget = () => { + const hits = [ { objectID: 123, _geoloc: { lat: 10, lng: 12 }, @@ -1373,13 +1370,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ __queryID: 'test-query-id', }, ]; - render = jest.fn(); + const render = jest.fn(); const unmount = jest.fn(); const customGeoSearch = connectGeoSearch(render, unmount); const widget = customGeoSearch(); - instantSearchInstance = createInstantSearch(); + const instantSearchInstance = createInstantSearch(); const { mainHelper: helper } = instantSearchInstance; widget.init({ @@ -1397,9 +1394,16 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ helper, instantSearchInstance, }); - }); + + return { + render, + instantSearchInstance, + hits, + }; + }; it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); @@ -1416,6 +1420,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); it('sends click event', () => { + const { instantSearchInstance, render, hits } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render @@ -1440,6 +1445,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); it('sends conversion event', () => { + const { instantSearchInstance, render, hits } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render diff --git a/src/connectors/hits/__tests__/connectHits-test.ts b/src/connectors/hits/__tests__/connectHits-test.ts index 4b25fc96ba..27036d1435 100644 --- a/src/connectors/hits/__tests__/connectHits-test.ts +++ b/src/connectors/hits/__tests__/connectHits-test.ts @@ -552,11 +552,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); describe('insights', () => { - let instantSearchInstance; - let renderFn; - let hits; - beforeEach(() => { - renderFn = jest.fn(); + const createRenderedWidget = () => { + const renderFn = jest.fn(); const makeWidget = connectHits(renderFn); const widget = makeWidget({}); @@ -567,10 +564,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co helper, state: helper.state, }); - instantSearchInstance = initOptions.instantSearchInstance; + const instantSearchInstance = initOptions.instantSearchInstance; widget.init!(initOptions); - hits = [ + const hits = [ { objectID: '1', fake: 'data', @@ -595,10 +592,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co helper, }) ); - }); + + return { instantSearchInstance, renderFn, hits }; + }; describe('sendEvent', () => { it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); @@ -615,6 +615,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); it('sends click event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render @@ -641,6 +646,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); it('sends conversion event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render @@ -668,6 +678,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co describe('bindEvent', () => { it('returns a payload for click event', () => { + const { renderFn, hits } = createRenderedWidget(); const { bindEvent } = renderFn.mock.calls[ renderFn.mock.calls.length - 1 ][0]; @@ -690,6 +701,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co }); it('returns a payload for conversion event', () => { + const { renderFn, hits } = createRenderedWidget(); const { bindEvent } = renderFn.mock.calls[ renderFn.mock.calls.length - 1 ][0]; diff --git a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts index 9f8583560d..c2d2b4b383 100644 --- a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts +++ b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts @@ -980,11 +980,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); describe('sendEvent & bindEvent', () => { - let instantSearchInstance; - let renderFn; - let hits; - beforeEach(() => { - renderFn = jest.fn(); + const createRenderedWidget = () => { + const renderFn = jest.fn(); const makeWidget = connectInfiniteHits(renderFn); const widget = makeWidget({}); @@ -995,10 +992,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi helper, state: helper.state, }); - instantSearchInstance = initOptions.instantSearchInstance; + const instantSearchInstance = initOptions.instantSearchInstance; widget.init!(initOptions); - hits = [ + const hits = [ { objectID: '1', fake: 'data', @@ -1023,10 +1020,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi helper, }) ); - }); + + return { instantSearchInstance, renderFn, hits }; + }; describe('sendEvent', () => { it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); @@ -1043,6 +1043,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); it('sends click event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render @@ -1069,6 +1074,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); it('sends conversion event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 ); // view event by render @@ -1096,6 +1106,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi describe('bindEvent', () => { it('returns a payload for click event', () => { + const { renderFn, hits } = createRenderedWidget(); const { bindEvent } = renderFn.mock.calls[ renderFn.mock.calls.length - 1 ][0]; @@ -1118,6 +1129,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); it('returns a payload for conversion event', () => { + const { renderFn, hits } = createRenderedWidget(); const { bindEvent } = renderFn.mock.calls[ renderFn.mock.calls.length - 1 ][0]; diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.js index 5f5c038bf2..6bcc26dc34 100644 --- a/src/connectors/menu/__tests__/connectMenu-test.js +++ b/src/connectors/menu/__tests__/connectMenu-test.js @@ -1089,14 +1089,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); describe('sendEvent', () => { - let instantSearchInstance; - let helper; - beforeEach(() => { + const createInitializedWidget = () => { const widget = makeWidget({ attribute: 'category', }); - instantSearchInstance = createInstantSearch(); - helper = jsHelper( + const instantSearchInstance = createInstantSearch(); + const helper = jsHelper( {}, '', widget.getWidgetSearchParameters(new SearchParameters(), { @@ -1111,9 +1109,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co createURL: () => '#', instantSearchInstance, }); - }); + + return { instantSearchInstance, helper }; + }; it('sends event when a facet is refined', () => { + const { instantSearchInstance } = createInitializedWidget(); const firstRenderingOptions = rendering.mock.calls[0][0]; const { refine } = firstRenderingOptions; refine('value'); @@ -1133,6 +1134,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); it('does not send event when a facet is removed', () => { + const { instantSearchInstance, helper } = createInitializedWidget(); const firstRenderingOptions = rendering.mock.calls[0][0]; const { refine } = firstRenderingOptions; refine('value'); diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js index 612a08dab1..479c0a019e 100644 --- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js +++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js @@ -2402,15 +2402,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- }); describe('sendEvent', () => { - let makeWidget; - let rendering; - let instantSearchInstance; - - beforeEach(() => { + const createInitializedWidget = () => { const factoryResult = createWidgetFactory(); - makeWidget = factoryResult.makeWidget; - rendering = factoryResult.rendering; - instantSearchInstance = createInstantSearch(); + const makeWidget = factoryResult.makeWidget; + const rendering = factoryResult.rendering; + const instantSearchInstance = createInstantSearch(); const widget = makeWidget({ attribute: 'category', }); @@ -2430,9 +2426,15 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- createURL: () => '#', instantSearchInstance, }); - }); + + return { + rendering, + instantSearchInstance, + }; + }; it('sends event when a facet is added', () => { + const { rendering, instantSearchInstance } = createInitializedWidget(); const firstRenderingOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = firstRenderingOptions; @@ -2453,6 +2455,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- }); it('does not send event when a facet is removed', () => { + const { rendering, instantSearchInstance } = createInitializedWidget(); const firstRenderingOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = firstRenderingOptions; diff --git a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js index d8545e16ac..e7fcc844b9 100644 --- a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js +++ b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js @@ -1093,22 +1093,17 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi }); describe('sendEvent', () => { - let rendering; - let helper; - let instantSearchInstance; - let widget; - - beforeEach(() => { - rendering = jest.fn(); - instantSearchInstance = createInstantSearch(); + const createInitializedWidget = () => { + const rendering = jest.fn(); + const instantSearchInstance = createInstantSearch(); const makeWidget = connectToggleRefinement(rendering); const attribute = 'isShippingFree'; - widget = makeWidget({ + const widget = makeWidget({ attribute, }); - helper = jsHelper( + const helper = jsHelper( {}, '', widget.getWidgetSearchParameters(new SearchParameters({}), { @@ -1116,10 +1111,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi }) ); helper.search = jest.fn(); - }); - it('sends event when a facet is added', () => { - // first rendering widget.init({ helper, state: helper.state, @@ -1127,6 +1119,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi instantSearchInstance, }); + return { rendering, helper, instantSearchInstance, widget }; + }; + + it('sends event when a facet is added', () => { + const { rendering, instantSearchInstance } = createInitializedWidget(); const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; @@ -1147,14 +1144,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi }); it('does not send event when a facet is removed', () => { - // first rendering - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - instantSearchInstance, - }); - + const { rendering, instantSearchInstance } = createInitializedWidget(); const renderOptions = rendering.mock.calls[rendering.mock.calls.length - 1][0]; const { refine } = renderOptions; diff --git a/src/lib/utils/__tests__/createSendEventForFacet-test.ts b/src/lib/utils/__tests__/createSendEventForFacet-test.ts index be960f978b..6d4e2d2428 100644 --- a/src/lib/utils/__tests__/createSendEventForFacet-test.ts +++ b/src/lib/utils/__tests__/createSendEventForFacet-test.ts @@ -1,29 +1,34 @@ import algoliasearchHelper from 'algoliasearch-helper'; import { createSendEventForFacet } from '../createSendEventForFacet'; -import { InstantSearch, SearchClient } from '../../../types'; +import { SearchClient } from '../../../types'; +import { createInstantSearch } from '../../../../test/mock/createInstantSearch'; jest.mock('../isFacetRefined', () => jest.fn()); import isFacetRefined from '../isFacetRefined'; -const instantSearchInstance = {} as InstantSearch; -let sendEvent; -let helper; -beforeEach(() => { - instantSearchInstance.sendEventToInsights = jest.fn(); - helper = algoliasearchHelper({} as SearchClient, '', {}); +const createTestEnvironment = () => { + const instantSearchInstance = createInstantSearch(); + const helper = algoliasearchHelper({} as SearchClient, '', {}); (isFacetRefined as jest.Mock).mockImplementation(() => false); - sendEvent = createSendEventForFacet({ + const sendEvent = createSendEventForFacet({ instantSearchInstance, helper, attribute: 'category', widgetType: 'ais.customWidget', }); -}); + + return { + instantSearchInstance, + helper, + sendEvent, + }; +}; describe('createSendEventForFacet', () => { describe('Usage', () => { it('throws when facetValue is missing', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('click'); }).toThrowErrorMatchingInlineSnapshot(` @@ -36,6 +41,7 @@ If you want to send a custom payload, you can pass one object: sendEvent(customP }); it('throws with unknown eventType', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('my custom event type'); }).toThrowErrorMatchingInlineSnapshot(` @@ -48,6 +54,7 @@ If you want to send a custom payload, you can pass one object: sendEvent(customP }); it('throws when eventType is not click', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('custom event type'); }).toThrowErrorMatchingInlineSnapshot(` @@ -60,6 +67,7 @@ If you want to send a custom payload, you can pass one object: sendEvent(customP }); it('does not send event when a facet is removed', () => { + const { sendEvent, instantSearchInstance } = createTestEnvironment(); (isFacetRefined as jest.Mock).mockImplementation(() => true); sendEvent('click', 'value'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( @@ -68,6 +76,7 @@ If you want to send a custom payload, you can pass one object: sendEvent(customP }); it('sends with default eventName', () => { + const { sendEvent, instantSearchInstance } = createTestEnvironment(); sendEvent('click', 'value'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 @@ -85,6 +94,7 @@ If you want to send a custom payload, you can pass one object: sendEvent(customP }); it('sends with custom eventName', () => { + const { sendEvent, instantSearchInstance } = createTestEnvironment(); sendEvent('click', 'value', 'Category Clicked'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( 1 diff --git a/src/lib/utils/__tests__/createSendEventForHits-test.ts b/src/lib/utils/__tests__/createSendEventForHits-test.ts index cb35296789..d2ef512bcc 100644 --- a/src/lib/utils/__tests__/createSendEventForHits-test.ts +++ b/src/lib/utils/__tests__/createSendEventForHits-test.ts @@ -2,41 +2,47 @@ import { createSendEventForHits, createBindEventForHits, } from '../createSendEventForHits'; -import { InstantSearch } from '../../../types'; - -const instantSearchInstance = {} as InstantSearch; -const index = 'testIndex'; -const widgetType = 'ais.testWidget'; -const hits = [ - { - objectID: 'obj0', - __position: 0, - __queryID: 'test-query-id', - }, - { - objectID: 'obj1', - __position: 1, - __queryID: 'test-query-id', - }, -]; - -beforeEach(() => { - instantSearchInstance.sendEventToInsights = jest.fn(); -}); - -describe('createSendEventForHits', () => { - let sendEvent; - - beforeEach(() => { - sendEvent = createSendEventForHits({ - instantSearchInstance, - index, - widgetType, - }); +import { createInstantSearch } from '../../../../test/mock/createInstantSearch'; + +const createTestEnvironment = () => { + const instantSearchInstance = createInstantSearch(); + const index = 'testIndex'; + const widgetType = 'ais.testWidget'; + const hits = [ + { + objectID: 'obj0', + __position: 0, + __queryID: 'test-query-id', + }, + { + objectID: 'obj1', + __position: 1, + __queryID: 'test-query-id', + }, + ]; + const sendEvent = createSendEventForHits({ + instantSearchInstance, + index, + widgetType, + }); + const bindEvent = createBindEventForHits({ + index, + widgetType, }); + return { + instantSearchInstance, + index, + widgetType, + hits, + sendEvent, + bindEvent, + }; +}; +describe('createSendEventForHits', () => { describe('Usage', () => { it('throws when hit is missing', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('click'); }).toThrowErrorMatchingInlineSnapshot(` @@ -47,6 +53,7 @@ describe('createSendEventForHits', () => { }); it('throws with unknown eventType', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('my custom event type'); }).toThrowErrorMatchingInlineSnapshot(` @@ -57,6 +64,7 @@ describe('createSendEventForHits', () => { }); it('throw when eventName is missing for click or conversion event', () => { + const { sendEvent } = createTestEnvironment(); expect(() => { sendEvent('click', {}); }).toThrowErrorMatchingInlineSnapshot(` @@ -80,6 +88,7 @@ describe('createSendEventForHits', () => { }); it('sends view event with default eventName', () => { + const { sendEvent, instantSearchInstance, hits } = createTestEnvironment(); sendEvent('view', hits[0]); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ @@ -95,6 +104,7 @@ describe('createSendEventForHits', () => { }); it('sends view event with custom eventName', () => { + const { sendEvent, instantSearchInstance, hits } = createTestEnvironment(); sendEvent('view', hits[0], 'Products Displayed'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ @@ -110,6 +120,7 @@ describe('createSendEventForHits', () => { }); it('sends view event with multiple hits', () => { + const { sendEvent, instantSearchInstance, hits } = createTestEnvironment(); sendEvent('view', hits); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ @@ -125,6 +136,7 @@ describe('createSendEventForHits', () => { }); it('sends click event', () => { + const { sendEvent, instantSearchInstance, hits } = createTestEnvironment(); sendEvent('click', hits[0], 'Product Clicked'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ @@ -142,6 +154,7 @@ describe('createSendEventForHits', () => { }); it('sends conversion event', () => { + const { sendEvent, instantSearchInstance, hits } = createTestEnvironment(); sendEvent('conversion', hits[0], 'Product Ordered'); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes(1); expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ @@ -158,6 +171,7 @@ describe('createSendEventForHits', () => { }); it('sends custom event', () => { + const { sendEvent, instantSearchInstance } = createTestEnvironment(); sendEvent({ hello: 'world', custom: 'event', @@ -171,21 +185,13 @@ describe('createSendEventForHits', () => { }); describe('createBindEventForHits', () => { - let bindEvent; - - beforeEach(() => { - bindEvent = createBindEventForHits({ - index, - widgetType, - }); - }); - function parsePayload(payload) { expect(payload.startsWith('data-insights-event=')).toBe(true); return JSON.parse(atob(payload.substr('data-insights-event='.length))); } it('returns a payload for click event', () => { + const { bindEvent, hits } = createTestEnvironment(); const parsedPayload = parsePayload( bindEvent('click', hits[0], 'Product Clicked') ); @@ -204,6 +210,7 @@ describe('createBindEventForHits', () => { }); it('returns a payload for conversion event', () => { + const { bindEvent, hits } = createTestEnvironment(); const parsedPayload = parsePayload( bindEvent('conversion', hits[0], 'Product Ordered') ); diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index e1714255b4..57c3ba9891 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -12,21 +12,21 @@ import { warning } from '../../../src/lib/utils'; import { SearchClient } from '../../types'; describe('insights', () => { - let analytics; - let insightsClient; - let instantSearchInstance; - let helper; - - beforeEach(() => { - analytics = createAlgoliaAnalytics(); - insightsClient = jest.fn(createInsightsClient(analytics)); - instantSearchInstance = createInstantSearch({ + const createTestEnvironment = () => { + const analytics = createAlgoliaAnalytics(); + const insightsClient = jest.fn(createInsightsClient(analytics)); + const instantSearchInstance = createInstantSearch({ client: algoliasearch('myAppId', 'myApiKey'), }); - helper = algoliasearchHelper({} as SearchClient, ''); + const helper = algoliasearchHelper({} as SearchClient, ''); instantSearchInstance.mainIndex = { getHelper: () => helper, }; + + return { analytics, insightsClient, instantSearchInstance, helper }; + }; + + beforeEach(() => { warning.cache = {}; }); @@ -51,6 +51,7 @@ describe('insights', () => { describe('initialize', () => { it('initialize insightsClient', () => { + const { insightsClient, instantSearchInstance } = createTestEnvironment(); expect.assertions(3); insightsClient('setUserToken', 'abc'); @@ -69,6 +70,7 @@ describe('insights', () => { }); it('warns dev if userToken is set before creating the middleware', () => { + const { insightsClient, instantSearchInstance } = createTestEnvironment(); insightsClient('setUserToken', 'abc'); expect(() => { createInsightsMiddleware({ @@ -86,6 +88,11 @@ aa('setUserToken', 'your-user-token');`); }); it('applies clickAnalytics', () => { + const { + insightsClient, + instantSearchInstance, + helper, + } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -96,6 +103,11 @@ aa('setUserToken', 'your-user-token');`); describe('userToken', () => { it('applies userToken which was set before subscribe()', () => { + const { + insightsClient, + instantSearchInstance, + helper, + } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -105,6 +117,11 @@ aa('setUserToken', 'your-user-token');`); }); it('applies userToken which was set after subscribe()', () => { + const { + insightsClient, + instantSearchInstance, + helper, + } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -114,6 +131,11 @@ aa('setUserToken', 'your-user-token');`); }); it('applies userToken from cookie when nothing given', () => { + const { + insightsClient, + instantSearchInstance, + helper, + } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -121,78 +143,117 @@ aa('setUserToken', 'your-user-token');`); expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); }); - it('applies userToken from queue if exists', () => { + it('ignores userToken set before init', () => { const { - insightsClient: localInsightsClient, - libraryLoadedAndProcessQueue, - } = createInsightsUmdVersion(); - - // call init and setUserToken even before the library is loaded. - localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); - localInsightsClient('setUserToken', 'token-from-queue'); - libraryLoadedAndProcessQueue(); + insightsClient, + instantSearchInstance, + helper, + } = createTestEnvironment(); - localInsightsClient('_get', '_userToken', userToken => { - expect(userToken).toEqual('token-from-queue'); - }); + insightsClient('setUserToken', 'token-from-queue-before-init'); const middleware = createInsightsMiddleware({ - insightsClient: localInsightsClient, + insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual('token-from-queue'); + expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); }); - it('applies userToken from queue even though the queue is not processed', () => { - const { - insightsClient: localInsightsClient, - } = createInsightsUmdVersion(); - - // call init and setUserToken even before the library is loaded. - localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); - localInsightsClient('setUserToken', 'token-from-queue'); + describe('umd', () => { + const createUmdTestEnvironment = () => { + const { + insightsClient, + libraryLoadedAndProcessQueue, + } = createInsightsUmdVersion(); + const instantSearchInstance = createInstantSearch({ + client: algoliasearch('myAppId', 'myApiKey'), + }); + const helper = algoliasearchHelper({} as SearchClient, ''); + instantSearchInstance.mainIndex = { + getHelper: () => helper, + }; + return { + insightsClient, + libraryLoadedAndProcessQueue, + instantSearchInstance, + helper, + }; + }; + it('applies userToken from queue if exists', () => { + const { + insightsClient, + libraryLoadedAndProcessQueue, + instantSearchInstance, + helper, + } = createUmdTestEnvironment(); + + // call init and setUserToken even before the library is loaded. + insightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + insightsClient('setUserToken', 'token-from-queue'); + libraryLoadedAndProcessQueue(); + + insightsClient('_get', '_userToken', userToken => { + expect(userToken).toEqual('token-from-queue'); + }); - localInsightsClient('_get', '_userToken', userToken => { - expect(userToken).toEqual('token-from-queue'); + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual('token-from-queue'); }); - const middleware = createInsightsMiddleware({ - insightsClient: localInsightsClient, - })({ instantSearchInstance }); - middleware.subscribe(); - expect(helper.state.userToken).toEqual('token-from-queue'); - }); + it('applies userToken from queue even though the queue is not processed', () => { + const { + insightsClient, + instantSearchInstance, + helper, + } = createUmdTestEnvironment(); - it('ignores userToken set before init (umd)', () => { - const { - insightsClient: localInsightsClient, - libraryLoadedAndProcessQueue, - } = createInsightsUmdVersion(); + // call init and setUserToken even before the library is loaded. + insightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + insightsClient('setUserToken', 'token-from-queue'); - localInsightsClient('setUserToken', 'token-from-queue-before-init'); - localInsightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); - libraryLoadedAndProcessQueue(); + insightsClient('_get', '_userToken', userToken => { + expect(userToken).toEqual('token-from-queue'); + }); - const middleware = createInsightsMiddleware({ - insightsClient: localInsightsClient, - })({ instantSearchInstance }); - middleware.subscribe(); - expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); - }); + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual('token-from-queue'); + }); - it('ignores userToken set before init (cjs)', () => { - insightsClient('setUserToken', 'token-from-queue-before-init'); + it('ignores userToken set before init', () => { + const { + insightsClient, + instantSearchInstance, + libraryLoadedAndProcessQueue, + helper, + } = createUmdTestEnvironment(); - const middleware = createInsightsMiddleware({ - insightsClient, - })({ instantSearchInstance }); - middleware.subscribe(); - expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + insightsClient('setUserToken', 'token-from-queue-before-init'); + insightsClient('init', { appId: 'myAppId', apiKey: 'myApiKey' }); + libraryLoadedAndProcessQueue(); + + const middleware = createInsightsMiddleware({ + insightsClient, + })({ instantSearchInstance }); + middleware.subscribe(); + expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + }); }); }); describe('sendEventToInsights', () => { it('sends events', () => { + const { + insightsClient, + instantSearchInstance, + analytics, + } = createTestEnvironment(); + const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -200,6 +261,8 @@ aa('setUserToken', 'your-user-token');`); instantSearchInstance.sendEventToInsights({ insightsMethod: 'viewedObjectIDs', + widgetType: 'ais.customWidget', + eventType: 'click', payload: { hello: 'world', }, @@ -211,6 +274,12 @@ aa('setUserToken', 'your-user-token');`); }); it('calls onEvent when given', () => { + const { + insightsClient, + instantSearchInstance, + analytics, + } = createTestEnvironment(); + const onEvent = jest.fn(); const middleware = createInsightsMiddleware({ insightsClient, @@ -220,6 +289,8 @@ aa('setUserToken', 'your-user-token');`); instantSearchInstance.sendEventToInsights({ insightsMethod: 'viewedObjectIDs', + widgetType: 'ais.customWidget', + eventType: 'click', payload: { hello: 'world', }, @@ -228,6 +299,8 @@ aa('setUserToken', 'your-user-token');`); expect(onEvent).toHaveBeenCalledTimes(1); expect(onEvent).toHaveBeenCalledWith({ insightsMethod: 'viewedObjectIDs', + widgetType: 'ais.customWidget', + eventType: 'click', payload: { hello: 'world', }, @@ -235,6 +308,8 @@ aa('setUserToken', 'your-user-token');`); }); it('warns dev when neither insightsMethod nor onEvent is given', () => { + const { insightsClient, instantSearchInstance } = createTestEnvironment(); + const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); @@ -243,6 +318,8 @@ aa('setUserToken', 'your-user-token');`); const numberOfCalls = insightsClient.mock.calls.length; expect(() => { instantSearchInstance.sendEventToInsights({ + widgetType: 'ais.customWidget', + eventType: 'click', payload: { hello: 'world', }, diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts index a2e9b4b901..3fc504d29c 100644 --- a/src/widgets/hits/__tests__/hits-integration-test.ts +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -5,56 +5,66 @@ import { hits, configure } from '../../'; import { createInsightsMiddleware } from '../../../middlewares'; import { createSingleSearchResponse } from '../../../../test/mock/createAPIResponse'; +const createInstantSearch = ({ hitsPerPage = 2 } = {}) => { + const page = 0; + + const searchClient = { + search: jest.fn(requests => + Promise.resolve({ + results: requests.map(() => + createSingleSearchResponse({ + hits: Array(hitsPerPage) + .fill(undefined) + .map((_, index) => ({ + title: `title ${page * hitsPerPage + index + 1}`, + objectID: `object-id${index}`, + })), + }) + ), + }) + ), + }; + const search = instantsearch({ + indexName: 'instant_search', + searchClient, + }); + + search.addWidgets([ + configure({ + hitsPerPage, + }), + ]); + + return { + search, + }; +}; + describe('hits', () => { - let search; - let searchClient; let container; - const hitsPerPage = 2; - const page = 0; beforeEach(() => { - searchClient = { - search: jest.fn(requests => - Promise.resolve({ - results: requests.map(() => - createSingleSearchResponse({ - hits: Array(hitsPerPage) - .fill(undefined) - .map((_, index) => ({ - title: `title ${page * hitsPerPage + index + 1}`, - objectID: `object-id${index}`, - })), - }) - ), - }) - ), - }; - search = instantsearch({ - indexName: 'instant_search', - searchClient, - }); - container = document.createElement('div'); }); describe('sendEvent', () => { - let onEvent; - beforeEach(() => { - onEvent = jest.fn(); + const createInsightsMiddlewareWithOnEvent = () => { + const onEvent = jest.fn(); const insights = createInsightsMiddleware({ insightsClient: null, onEvent, }); - search.EXPERIMENTAL_use(insights); - - search.addWidgets([ - configure({ - hitsPerPage, - }), - ]); - }); + return { + onEvent, + insights, + }; + }; it('sends view event when hits are rendered', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ hits({ container, @@ -78,6 +88,10 @@ describe('hits', () => { }); it('sends click event', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ hits({ container, @@ -111,6 +125,10 @@ describe('hits', () => { }); it('sends conversion event', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ hits({ container, diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 6301a56b62..735a70c023 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -18,23 +18,30 @@ function createSingleSearchResponse({ params: { hitsPerPage, page } }) { } describe('infiniteHits', () => { - let search; - let searchClient; - let container; - - beforeEach(() => { - searchClient = { + const createInstantSearch = ({ hitsPerPage = 2 } = {}) => { + const searchClient = { search: jest.fn(requests => Promise.resolve({ results: requests.map(request => createSingleSearchResponse(request)), }) ), }; - search = instantsearch({ + const search = instantsearch({ indexName: 'instant_search', searchClient, }); + search.addWidgets([ + configure({ + hitsPerPage, + }), + ]); + + return { search, searchClient }; + }; + let container; + + beforeEach(() => { container = document.createElement('div'); }); @@ -65,10 +72,9 @@ describe('infiniteHits', () => { }); it('calls read & write methods of custom cache', async () => { + const { search } = createInstantSearch(); + search.addWidgets([ - configure({ - hitsPerPage: 2, - }), infiniteHits({ container, cache: customCache, @@ -96,11 +102,10 @@ describe('infiniteHits', () => { }); it('displays all the hits from cache', async () => { + const { search, searchClient } = createInstantSearch(); + // flow #1 - load page #0 & #1 to fill the cache search.addWidgets([ - configure({ - hitsPerPage: 2, - }), infiniteHits({ container, cache: customCache, @@ -149,23 +154,23 @@ describe('infiniteHits', () => { }); describe('sendEvent', () => { - let onEvent; - beforeEach(() => { - onEvent = jest.fn(); + const createInsightsMiddlewareWithOnEvent = () => { + const onEvent = jest.fn(); const insights = createInsightsMiddleware({ insightsClient: null, onEvent, }); - search.EXPERIMENTAL_use(insights); - - search.addWidgets([ - configure({ - hitsPerPage: 2, - }), - ]); - }); + return { + onEvent, + insights, + }; + }; it('sends view event when hits are rendered', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ infiniteHits({ container, @@ -189,6 +194,10 @@ describe('infiniteHits', () => { }); it('sends click event', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ infiniteHits({ container, @@ -222,6 +231,10 @@ describe('infiniteHits', () => { }); it('sends conversion event', done => { + const { search } = createInstantSearch(); + const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); + search.EXPERIMENTAL_use(insights); + search.addWidgets([ infiniteHits({ container, From 8300a1db7678224ab1509013da817d8500e6cc2e Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Fri, 28 Aug 2020 18:04:36 +0200 Subject: [PATCH 17/23] clean up --- test/mock/createInstantSearch.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/mock/createInstantSearch.ts b/test/mock/createInstantSearch.ts index ec07c5feb5..652bd8966f 100644 --- a/test/mock/createInstantSearch.ts +++ b/test/mock/createInstantSearch.ts @@ -7,16 +7,15 @@ import defer from '../../src/lib/utils/defer'; export const createInstantSearch = ( args: Partial = {} ): InstantSearch => { - const searchClient = args.client || createSearchClient(); - const { indexName = 'indexName' } = args; - const mainHelper = algoliasearchHelper(searchClient, indexName, {}); + const { indexName = 'indexName', client = createSearchClient() } = args; + const mainHelper = algoliasearchHelper(client, indexName, {}); const mainIndex = index({ indexName }); return { indexName, mainIndex, mainHelper, - client: searchClient, + client, started: false, start() { this.started = true; From 2be5e150136d92230364642966906197589679f8 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Fri, 28 Aug 2020 18:09:09 +0200 Subject: [PATCH 18/23] update comment --- src/middlewares/insights.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/middlewares/insights.ts b/src/middlewares/insights.ts index 0f58098dc2..bfb7770b0f 100644 --- a/src/middlewares/insights.ts +++ b/src/middlewares/insights.ts @@ -86,13 +86,10 @@ aa('setUserToken', 'your-user-token'); // ['setUserToken', 'my-user-token'] gets stored in `aa.queue`. // Whenever `search-insights` is finally loaded, it will process the queue. // - // But the reason why we handle it here is - // (1) At this point, even though `search-insights` is not loaded yet, + // But here's the reason why we handle it here: + // At this point, even though `search-insights` is not loaded yet, // we still want to read the token from the queue. // Otherwise, the first search call will be fired without the token. - // (2) Or, user could use a customized client of `search-insights`, - // for example, by using `createInsightsClient` function. - // Then `processQueue` might not be called. But we still want to read the token from the queue. (insightsClient as any).queue.forEach(([method, firstArgument]) => { if (method === 'setUserToken') { setUserTokenToSearch(firstArgument); From 6e235fb83e7911d21b92effad18097a5ac9b95ba Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Fri, 28 Aug 2020 18:33:25 +0200 Subject: [PATCH 19/23] use runAllMicroTasks instead of nextTick --- .../hits/__tests__/hits-integration-test.ts | 92 +++++++++---------- .../infinite-hits-integration-test.ts | 92 +++++++++---------- 2 files changed, 90 insertions(+), 94 deletions(-) diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts index 3fc504d29c..9782c41c80 100644 --- a/src/widgets/hits/__tests__/hits-integration-test.ts +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -4,6 +4,7 @@ import instantsearch from '../../../index.es'; import { hits, configure } from '../../'; import { createInsightsMiddleware } from '../../../middlewares'; import { createSingleSearchResponse } from '../../../../test/mock/createAPIResponse'; +import { runAllMicroTasks } from '../../../../test/utils/runAllMicroTasks'; const createInstantSearch = ({ hitsPerPage = 2 } = {}) => { const page = 0; @@ -60,7 +61,7 @@ describe('hits', () => { }; }; - it('sends view event when hits are rendered', done => { + it('sends view event when hits are rendered', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -71,23 +72,22 @@ describe('hits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); - expect(onEvent).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: 'instant_search', - objectIDs: ['object-id0', 'object-id1'], - }, - widgetType: 'ais.hits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.hits', }); }); - it('sends click event', done => { + it('sends click event', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -105,26 +105,25 @@ describe('hits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); // view event by render - fireEvent.click(getByText(container, 'title 1')); - expect(onEvent).toHaveBeenCalledTimes(2); - expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Item Clicked', - index: 'instant_search', - objectIDs: ['object-id0'], - positions: [1], - }, - widgetType: 'ais.hits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 1')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Item Clicked', + index: 'instant_search', + objectIDs: ['object-id0'], + positions: [1], + }, + widgetType: 'ais.hits', }); }); - it('sends conversion event', done => { + it('sends conversion event', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -146,21 +145,20 @@ describe('hits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); // view event by render - fireEvent.click(getByText(container, 'title 2')); - expect(onEvent).toHaveBeenCalledTimes(2); - expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: 'instant_search', - objectIDs: ['object-id1'], - }, - widgetType: 'ais.hits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 2')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'instant_search', + objectIDs: ['object-id1'], + }, + widgetType: 'ais.hits', }); }); }); diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 735a70c023..4ddd2ba11b 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -3,6 +3,7 @@ import { getByText, waitFor, fireEvent } from '@testing-library/dom'; import instantsearch from '../../../index.es'; import { infiniteHits, configure } from '../../'; import { createInsightsMiddleware } from '../../../middlewares'; +import { runAllMicroTasks } from '../../../../test/utils/runAllMicroTasks'; function createSingleSearchResponse({ params: { hitsPerPage, page } }) { return { @@ -166,7 +167,7 @@ describe('infiniteHits', () => { }; }; - it('sends view event when hits are rendered', done => { + it('sends view event when hits are rendered', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -177,23 +178,22 @@ describe('infiniteHits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); - expect(onEvent).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: 'instant_search', - objectIDs: ['object-id0', 'object-id1'], - }, - widgetType: 'ais.infiniteHits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.infiniteHits', }); }); - it('sends click event', done => { + it('sends click event', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -211,26 +211,25 @@ describe('infiniteHits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); // view event by render - fireEvent.click(getByText(container, 'title 1')); - expect(onEvent).toHaveBeenCalledTimes(2); - expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Item Clicked', - index: 'instant_search', - objectIDs: ['object-id0'], - positions: [1], - }, - widgetType: 'ais.infiniteHits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 1')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Item Clicked', + index: 'instant_search', + objectIDs: ['object-id0'], + positions: [1], + }, + widgetType: 'ais.infiniteHits', }); }); - it('sends conversion event', done => { + it('sends conversion event', async () => { const { search } = createInstantSearch(); const { insights, onEvent } = createInsightsMiddlewareWithOnEvent(); search.EXPERIMENTAL_use(insights); @@ -252,21 +251,20 @@ describe('infiniteHits', () => { }), ]); search.start(); - process.nextTick(() => { - expect(onEvent).toHaveBeenCalledTimes(1); // view event by render - fireEvent.click(getByText(container, 'title 2')); - expect(onEvent).toHaveBeenCalledTimes(2); - expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: 'instant_search', - objectIDs: ['object-id1'], - }, - widgetType: 'ais.infiniteHits', - }); - done(); + await runAllMicroTasks(); + + expect(onEvent).toHaveBeenCalledTimes(1); // view event by render + fireEvent.click(getByText(container, 'title 2')); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[onEvent.mock.calls.length - 1][0]).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: 'instant_search', + objectIDs: ['object-id1'], + }, + widgetType: 'ais.infiniteHits', }); }); }); From 6939622a29553b09ed45f46762bb4b1f914bac4b Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 31 Aug 2020 11:08:39 +0200 Subject: [PATCH 20/23] fix: type errors --- .../__tests__/connectAutocomplete-test.ts | 45 ++++++----------- .../__tests__/createSendEventForHits-test.ts | 4 +- src/middlewares/__tests__/insights.ts | 48 ++++++++++++------- .../hits/__tests__/hits-integration-test.ts | 2 +- .../infinite-hits-integration-test.ts | 2 +- 5 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts index 7d6dcc7471..122f9da0bd 100644 --- a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts +++ b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts @@ -631,8 +631,11 @@ search.addWidgets([ createRenderOptions({ instantSearchInstance, helper, scopedResults }) ); + const sendEventToInsights = instantSearchInstance.sendEventToInsights as jest.Mock; + return { instantSearchInstance, + sendEventToInsights, render, firstIndexHits, secondIndexHits, @@ -640,13 +643,9 @@ search.addWidgets([ }; it('sends view event when hits are rendered', () => { - const { instantSearchInstance } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); - expect( - instantSearchInstance.sendEventToInsights.mock.calls[0][0] - ).toEqual({ + const { sendEventToInsights } = createRenderedWidget(); + expect(sendEventToInsights).toHaveBeenCalledTimes(2); + expect(sendEventToInsights.mock.calls[0][0]).toEqual({ eventType: 'view', insightsMethod: 'viewedObjectIDs', payload: { @@ -656,9 +655,7 @@ search.addWidgets([ }, widgetType: 'ais.autocomplete', }); - expect( - instantSearchInstance.sendEventToInsights.mock.calls[1][0] - ).toEqual({ + expect(sendEventToInsights.mock.calls[1][0]).toEqual({ eventType: 'view', insightsMethod: 'viewedObjectIDs', payload: { @@ -672,22 +669,16 @@ search.addWidgets([ it('sends click event', () => { const { - instantSearchInstance, + sendEventToInsights, render, secondIndexHits, } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); // two view events for each index by render + expect(sendEventToInsights).toHaveBeenCalledTimes(2); // two view events for each index by render const { indices } = render.mock.calls[render.mock.calls.length - 1][0]; indices[1].sendEvent('click', secondIndexHits[0], 'Product Added'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 3 - ); - expect( - instantSearchInstance.sendEventToInsights.mock.calls[2][0] - ).toEqual({ + expect(sendEventToInsights).toHaveBeenCalledTimes(3); + expect(sendEventToInsights.mock.calls[2][0]).toEqual({ eventType: 'click', insightsMethod: 'clickedObjectIDsAfterSearch', payload: { @@ -703,22 +694,16 @@ search.addWidgets([ it('sends conversion event', () => { const { - instantSearchInstance, + sendEventToInsights, render, firstIndexHits, } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); // two view events for each index by render + expect(sendEventToInsights).toHaveBeenCalledTimes(2); // two view events for each index by render const { indices } = render.mock.calls[render.mock.calls.length - 1][0]; indices[0].sendEvent('conversion', firstIndexHits[0], 'Product Ordered'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 3 - ); - expect( - instantSearchInstance.sendEventToInsights.mock.calls[2][0] - ).toEqual({ + expect(sendEventToInsights).toHaveBeenCalledTimes(3); + expect(sendEventToInsights.mock.calls[2][0]).toEqual({ eventType: 'conversion', insightsMethod: 'convertedObjectIDsAfterSearch', payload: { diff --git a/src/lib/utils/__tests__/createSendEventForHits-test.ts b/src/lib/utils/__tests__/createSendEventForHits-test.ts index d2ef512bcc..a793b1de84 100644 --- a/src/lib/utils/__tests__/createSendEventForHits-test.ts +++ b/src/lib/utils/__tests__/createSendEventForHits-test.ts @@ -66,7 +66,7 @@ describe('createSendEventForHits', () => { it('throw when eventName is missing for click or conversion event', () => { const { sendEvent } = createTestEnvironment(); expect(() => { - sendEvent('click', {}); + sendEvent('click', {} as any); }).toThrowErrorMatchingInlineSnapshot(` "You need to pass eventName as the third argument for 'click' or 'conversion' events like: sendEvent('click', hit, 'Product Purchased'); @@ -76,7 +76,7 @@ describe('createSendEventForHits', () => { `); expect(() => { - sendEvent('conversion', {}); + sendEvent('conversion', {} as any); }).toThrowErrorMatchingInlineSnapshot(` "You need to pass eventName as the third argument for 'click' or 'conversion' events like: sendEvent('click', hit, 'Product Purchased'); diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index 57c3ba9891..9b999bf6db 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -10,6 +10,7 @@ import { } from '../../../test/mock/createInsightsClient'; import { warning } from '../../../src/lib/utils'; import { SearchClient } from '../../types'; +import { Index } from '../../widgets/index/index'; describe('insights', () => { const createTestEnvironment = () => { @@ -19,11 +20,20 @@ describe('insights', () => { client: algoliasearch('myAppId', 'myApiKey'), }); const helper = algoliasearchHelper({} as SearchClient, ''); + const getUserToken = () => { + return (helper.state as any).userToken; + }; instantSearchInstance.mainIndex = { getHelper: () => helper, + } as Index; + + return { + analytics, + insightsClient, + instantSearchInstance, + helper, + getUserToken, }; - - return { analytics, insightsClient, instantSearchInstance, helper }; }; beforeEach(() => { @@ -106,48 +116,48 @@ aa('setUserToken', 'your-user-token');`); const { insightsClient, instantSearchInstance, - helper, + getUserToken, } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); insightsClient('setUserToken', 'abc'); middleware.subscribe(); - expect(helper.state.userToken).toEqual('abc'); + expect(getUserToken()).toEqual('abc'); }); it('applies userToken which was set after subscribe()', () => { const { insightsClient, instantSearchInstance, - helper, + getUserToken, } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); middleware.subscribe(); insightsClient('setUserToken', 'def'); - expect(helper.state.userToken).toEqual('def'); + expect(getUserToken()).toEqual('def'); }); it('applies userToken from cookie when nothing given', () => { const { insightsClient, instantSearchInstance, - helper, + getUserToken, } = createTestEnvironment(); const middleware = createInsightsMiddleware({ insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + expect(getUserToken()).toEqual(ANONYMOUS_TOKEN); }); it('ignores userToken set before init', () => { const { insightsClient, instantSearchInstance, - helper, + getUserToken, } = createTestEnvironment(); insightsClient('setUserToken', 'token-from-queue-before-init'); @@ -156,7 +166,7 @@ aa('setUserToken', 'your-user-token');`); insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + expect(getUserToken()).toEqual(ANONYMOUS_TOKEN); }); describe('umd', () => { @@ -169,14 +179,18 @@ aa('setUserToken', 'your-user-token');`); client: algoliasearch('myAppId', 'myApiKey'), }); const helper = algoliasearchHelper({} as SearchClient, ''); + const getUserToken = () => { + return (helper.state as any).userToken; + }; instantSearchInstance.mainIndex = { getHelper: () => helper, - }; + } as Index; return { insightsClient, libraryLoadedAndProcessQueue, instantSearchInstance, helper, + getUserToken, }; }; it('applies userToken from queue if exists', () => { @@ -184,7 +198,7 @@ aa('setUserToken', 'your-user-token');`); insightsClient, libraryLoadedAndProcessQueue, instantSearchInstance, - helper, + getUserToken, } = createUmdTestEnvironment(); // call init and setUserToken even before the library is loaded. @@ -200,14 +214,14 @@ aa('setUserToken', 'your-user-token');`); insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual('token-from-queue'); + expect(getUserToken()).toEqual('token-from-queue'); }); it('applies userToken from queue even though the queue is not processed', () => { const { insightsClient, instantSearchInstance, - helper, + getUserToken, } = createUmdTestEnvironment(); // call init and setUserToken even before the library is loaded. @@ -222,7 +236,7 @@ aa('setUserToken', 'your-user-token');`); insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual('token-from-queue'); + expect(getUserToken()).toEqual('token-from-queue'); }); it('ignores userToken set before init', () => { @@ -230,7 +244,7 @@ aa('setUserToken', 'your-user-token');`); insightsClient, instantSearchInstance, libraryLoadedAndProcessQueue, - helper, + getUserToken, } = createUmdTestEnvironment(); insightsClient('setUserToken', 'token-from-queue-before-init'); @@ -241,7 +255,7 @@ aa('setUserToken', 'your-user-token');`); insightsClient, })({ instantSearchInstance }); middleware.subscribe(); - expect(helper.state.userToken).toEqual(ANONYMOUS_TOKEN); + expect(getUserToken()).toEqual(ANONYMOUS_TOKEN); }); }); }); diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts index 9782c41c80..5e1c598655 100644 --- a/src/widgets/hits/__tests__/hits-integration-test.ts +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -9,7 +9,7 @@ import { runAllMicroTasks } from '../../../../test/utils/runAllMicroTasks'; const createInstantSearch = ({ hitsPerPage = 2 } = {}) => { const page = 0; - const searchClient = { + const searchClient: any = { search: jest.fn(requests => Promise.resolve({ results: requests.map(() => diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 4ddd2ba11b..4105c51a7d 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -20,7 +20,7 @@ function createSingleSearchResponse({ params: { hitsPerPage, page } }) { describe('infiniteHits', () => { const createInstantSearch = ({ hitsPerPage = 2 } = {}) => { - const searchClient = { + const searchClient: any = { search: jest.fn(requests => Promise.resolve({ results: requests.map(request => createSingleSearchResponse(request)), From 64a610a8b6022b70b67105bf60c7953c1f0afadc Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Mon, 31 Aug 2020 23:49:22 +0200 Subject: [PATCH 21/23] fix wrong import paths --- src/lib/insights/listener.tsx | 2 +- src/lib/utils/createSendEventForHits.ts | 2 +- src/middlewares/__tests__/insights.ts | 2 +- src/widgets/hits/hits.tsx | 2 +- src/widgets/infinite-hits/infinite-hits.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/insights/listener.tsx b/src/lib/insights/listener.tsx index 4146777f3b..58c2ea7d7b 100644 --- a/src/lib/insights/listener.tsx +++ b/src/lib/insights/listener.tsx @@ -3,7 +3,7 @@ import { h } from 'preact'; import { readDataAttributes, hasDataAttributes } from '../../helpers/insights'; import { InsightsClientWrapper } from '../../types'; -import { InsightsEvent } from '../../middlewares/insights'; +import { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; type WithInsightsListenerProps = { [key: string]: unknown; diff --git a/src/lib/utils/createSendEventForHits.ts b/src/lib/utils/createSendEventForHits.ts index a7541aa572..2d654d0718 100644 --- a/src/lib/utils/createSendEventForHits.ts +++ b/src/lib/utils/createSendEventForHits.ts @@ -1,5 +1,5 @@ import { InstantSearch, Hit } from '../../types'; -import { InsightsEvent } from '../../middlewares/insights'; +import { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; type BuiltInSendEventForHits = ( eventType: string, diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/insights.ts index 9b999bf6db..7d96c442cd 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/insights.ts @@ -1,6 +1,6 @@ import algoliasearch from 'algoliasearch'; import algoliasearchHelper from 'algoliasearch-helper'; -import { createInsightsMiddleware } from '../insights'; +import { createInsightsMiddleware } from '../createInsightsMiddleware'; import { createInstantSearch } from '../../../test/mock/createInstantSearch'; import { createAlgoliaAnalytics, diff --git a/src/widgets/hits/hits.tsx b/src/widgets/hits/hits.tsx index 454dea9b4d..ac6c2c71b7 100644 --- a/src/widgets/hits/hits.tsx +++ b/src/widgets/hits/hits.tsx @@ -23,7 +23,7 @@ import { Renderer, InsightsClientWrapper, } from '../../types'; -import { InsightsEvent } from '../../middlewares/insights'; +import { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; const withUsage = createDocumentationMessageGenerator({ name: 'hits' }); const suit = component('Hits'); diff --git a/src/widgets/infinite-hits/infinite-hits.tsx b/src/widgets/infinite-hits/infinite-hits.tsx index 92c00b5268..0496598643 100644 --- a/src/widgets/infinite-hits/infinite-hits.tsx +++ b/src/widgets/infinite-hits/infinite-hits.tsx @@ -25,7 +25,7 @@ import { Renderer, } from '../../types'; import defaultTemplates from './defaultTemplates'; -import { InsightsEvent } from '../../middlewares/insights'; +import { InsightsEvent } from '../../middlewares/createInsightsMiddleware'; const withUsage = createDocumentationMessageGenerator({ name: 'infinite-hits', From fd3f9780c39d1b31842cf5c8b2608b5ba72aa058 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Tue, 1 Sep 2020 23:55:55 +0200 Subject: [PATCH 22/23] fix: pass insightsClient to onEvent as the second parameter --- ...nsights.ts => createInsightsMiddleware.ts} | 19 ++++++++++------- src/middlewares/createInsightsMiddleware.ts | 7 +++++-- .../hits/__tests__/hits-integration-test.ts | 21 +++++++++++-------- .../infinite-hits-integration-test.ts | 21 +++++++++++-------- 4 files changed, 40 insertions(+), 28 deletions(-) rename src/middlewares/__tests__/{insights.ts => createInsightsMiddleware.ts} (97%) diff --git a/src/middlewares/__tests__/insights.ts b/src/middlewares/__tests__/createInsightsMiddleware.ts similarity index 97% rename from src/middlewares/__tests__/insights.ts rename to src/middlewares/__tests__/createInsightsMiddleware.ts index 7d96c442cd..1cd291fa49 100644 --- a/src/middlewares/__tests__/insights.ts +++ b/src/middlewares/__tests__/createInsightsMiddleware.ts @@ -8,7 +8,7 @@ import { createInsightsUmdVersion, ANONYMOUS_TOKEN, } from '../../../test/mock/createInsightsClient'; -import { warning } from '../../../src/lib/utils'; +import { warning } from '../../lib/utils'; import { SearchClient } from '../../types'; import { Index } from '../../widgets/index/index'; @@ -311,14 +311,17 @@ aa('setUserToken', 'your-user-token');`); }); expect(analytics.viewedObjectIDs).toHaveBeenCalledTimes(0); expect(onEvent).toHaveBeenCalledTimes(1); - expect(onEvent).toHaveBeenCalledWith({ - insightsMethod: 'viewedObjectIDs', - widgetType: 'ais.customWidget', - eventType: 'click', - payload: { - hello: 'world', + expect(onEvent).toHaveBeenCalledWith( + { + insightsMethod: 'viewedObjectIDs', + widgetType: 'ais.customWidget', + eventType: 'click', + payload: { + hello: 'world', + }, }, - }); + insightsClient + ); }); it('warns dev when neither insightsMethod nor onEvent is given', () => { diff --git a/src/middlewares/createInsightsMiddleware.ts b/src/middlewares/createInsightsMiddleware.ts index bfb7770b0f..077affc4ba 100644 --- a/src/middlewares/createInsightsMiddleware.ts +++ b/src/middlewares/createInsightsMiddleware.ts @@ -11,7 +11,10 @@ export type InsightsEvent = { export type InsightsProps = { insightsClient: null | InsightsClient; - onEvent?: (event: InsightsEvent) => void; + onEvent?: ( + event: InsightsEvent, + insightsClient: null | InsightsClient + ) => void; }; export type CreateInsightsMiddleware = (props: InsightsProps) => Middleware; @@ -104,7 +107,7 @@ aa('setUserToken', 'your-user-token'); instantSearchInstance.sendEventToInsights = (event: InsightsEvent) => { if (onEvent) { - onEvent(event); + onEvent(event, _insightsClient); } else if (event.insightsMethod) { insightsClient(event.insightsMethod, event.payload); } else { diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts index 5e1c598655..9bc041591a 100644 --- a/src/widgets/hits/__tests__/hits-integration-test.ts +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -75,16 +75,19 @@ describe('hits', () => { await runAllMicroTasks(); expect(onEvent).toHaveBeenCalledTimes(1); - expect(onEvent).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: 'instant_search', - objectIDs: ['object-id0', 'object-id1'], + expect(onEvent).toHaveBeenCalledWith( + { + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.hits', }, - widgetType: 'ais.hits', - }); + null + ); }); it('sends click event', async () => { diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 4105c51a7d..87b8848810 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -181,16 +181,19 @@ describe('infiniteHits', () => { await runAllMicroTasks(); expect(onEvent).toHaveBeenCalledTimes(1); - expect(onEvent).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: 'instant_search', - objectIDs: ['object-id0', 'object-id1'], + expect(onEvent).toHaveBeenCalledWith( + { + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: 'instant_search', + objectIDs: ['object-id0', 'object-id1'], + }, + widgetType: 'ais.infiniteHits', }, - widgetType: 'ais.infiniteHits', - }); + null + ); }); it('sends click event', async () => { From d228af5a6ef1b258712c7a4762babd648bbc56b8 Mon Sep 17 00:00:00 2001 From: eunjae-lee Date: Wed, 2 Sep 2020 13:26:39 +0200 Subject: [PATCH 23/23] update titles of describe blocks --- .../__tests__/connectAutocomplete-test.ts | 2 +- .../__tests__/connectGeoSearch-test.js | 2 +- .../__tests__/connectHierarchicalMenu-test.js | 2 +- .../hits/__tests__/connectHits-test.ts | 236 ++++++++--------- .../__tests__/connectInfiniteHits-test.ts | 238 +++++++++--------- .../menu/__tests__/connectMenu-test.js | 2 +- .../__tests__/connectNumericMenu-test.ts | 2 +- .../range/__tests__/connectRange-test.js | 2 +- .../__tests__/connectRatingMenu-test.js | 2 +- .../__tests__/connectRefinementList-test.js | 2 +- .../__tests__/connectToggleRefinement-test.js | 2 +- .../hits/__tests__/hits-integration-test.ts | 2 +- .../infinite-hits-integration-test.ts | 2 +- 13 files changed, 256 insertions(+), 240 deletions(-) diff --git a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts index 122f9da0bd..90c815af08 100644 --- a/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts +++ b/src/connectors/autocomplete/__tests__/connectAutocomplete-test.ts @@ -567,7 +567,7 @@ search.addWidgets([ }); }); - describe('sendEvent', () => { + describe('insights', () => { const createRenderedWidget = () => { const searchClient = createSearchClient(); const render = jest.fn(); diff --git a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js index 3e006f976f..4a4e977c95 100644 --- a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js +++ b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js @@ -1348,7 +1348,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/geo-search/ }); }); - describe('sendEvent', () => { + describe('insights', () => { const createRenderedWidget = () => { const hits = [ { diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js index 66b3235815..c0a1026709 100644 --- a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js +++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js @@ -1129,7 +1129,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica }); }); - describe('sendEvent', () => { + describe('insights', () => { it('sends event when a facet is added', () => { const rendering = jest.fn(); const instantSearchInstance = createInstantSearch(); diff --git a/src/connectors/hits/__tests__/connectHits-test.ts b/src/connectors/hits/__tests__/connectHits-test.ts index 27036d1435..fd911b920c 100644 --- a/src/connectors/hits/__tests__/connectHits-test.ts +++ b/src/connectors/hits/__tests__/connectHits-test.ts @@ -596,129 +596,137 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/#co return { instantSearchInstance, renderFn, hits }; }; - describe('sendEvent', () => { - it('sends view event when hits are rendered', () => { - const { instantSearchInstance } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: '', - objectIDs: ['1', '2'], - }, - widgetType: 'ais.hits', + describe('insights', () => { + describe('sendEvent', () => { + it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: '', + objectIDs: ['1', '2'], + }, + widgetType: 'ais.hits', + }); }); - }); - it('sends click event', () => { - const { - instantSearchInstance, - renderFn, - hits, - } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); // view event by render - - const { sendEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - sendEvent('click', hits[0], 'Product Added'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Product Added', - index: '', - objectIDs: ['1'], - positions: [0], - queryID: 'test-query-id', - }, - widgetType: 'ais.hits', + it('sends click event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('click', hits[0], 'Product Added'); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(2); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); }); - }); - it('sends conversion event', () => { - const { - instantSearchInstance, - renderFn, - hits, - } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); // view event by render - - const { sendEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - sendEvent('conversion', hits[1], 'Product Ordered'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: '', - objectIDs: ['2'], - queryID: 'test-query-id', - }, - widgetType: 'ais.hits', + it('sends conversion event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('conversion', hits[1], 'Product Ordered'); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(2); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); }); }); - }); - describe('bindEvent', () => { - it('returns a payload for click event', () => { - const { renderFn, hits } = createRenderedWidget(); - const { bindEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - const payload = bindEvent('click', hits[0], 'Product Added'); - expect(payload.startsWith('data-insights-event=')).toBe(true); - expect( - JSON.parse(atob(payload.substr('data-insights-event='.length))) - ).toEqual({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Product Added', - index: '', - objectIDs: ['1'], - positions: [0], - queryID: 'test-query-id', - }, - widgetType: 'ais.hits', + describe('bindEvent', () => { + it('returns a payload for click event', () => { + const { renderFn, hits } = createRenderedWidget(); + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('click', hits[0], 'Product Added'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); }); - }); - it('returns a payload for conversion event', () => { - const { renderFn, hits } = createRenderedWidget(); - const { bindEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - const payload = bindEvent('conversion', hits[1], 'Product Ordered'); - expect(payload.startsWith('data-insights-event=')).toBe(true); - expect( - JSON.parse(atob(payload.substr('data-insights-event='.length))) - ).toEqual({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: '', - objectIDs: ['2'], - queryID: 'test-query-id', - }, - widgetType: 'ais.hits', + it('returns a payload for conversion event', () => { + const { renderFn, hits } = createRenderedWidget(); + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('conversion', hits[1], 'Product Ordered'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.hits', + }); }); }); }); diff --git a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts index c2d2b4b383..4dab82d433 100644 --- a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts +++ b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.ts @@ -979,7 +979,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi }); }); - describe('sendEvent & bindEvent', () => { + describe('insights', () => { const createRenderedWidget = () => { const renderFn = jest.fn(); const makeWidget = connectInfiniteHits(renderFn); @@ -1024,129 +1024,137 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/infinite-hi return { instantSearchInstance, renderFn, hits }; }; - describe('sendEvent', () => { - it('sends view event when hits are rendered', () => { - const { instantSearchInstance } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'view', - insightsMethod: 'viewedObjectIDs', - payload: { - eventName: 'Hits Viewed', - index: '', - objectIDs: ['1', '2'], - }, - widgetType: 'ais.infiniteHits', + describe('insights', () => { + describe('sendEvent', () => { + it('sends view event when hits are rendered', () => { + const { instantSearchInstance } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'view', + insightsMethod: 'viewedObjectIDs', + payload: { + eventName: 'Hits Viewed', + index: '', + objectIDs: ['1', '2'], + }, + widgetType: 'ais.infiniteHits', + }); }); - }); - it('sends click event', () => { - const { - instantSearchInstance, - renderFn, - hits, - } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); // view event by render - - const { sendEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - sendEvent('click', hits[0], 'Product Added'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Product Added', - index: '', - objectIDs: ['1'], - positions: [0], - queryID: 'test-query-id', - }, - widgetType: 'ais.infiniteHits', + it('sends click event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('click', hits[0], 'Product Added'); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(2); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); }); - }); - it('sends conversion event', () => { - const { - instantSearchInstance, - renderFn, - hits, - } = createRenderedWidget(); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 1 - ); // view event by render - - const { sendEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - sendEvent('conversion', hits[1], 'Product Ordered'); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledTimes( - 2 - ); - expect(instantSearchInstance.sendEventToInsights).toHaveBeenCalledWith({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: '', - objectIDs: ['2'], - queryID: 'test-query-id', - }, - widgetType: 'ais.infiniteHits', + it('sends conversion event', () => { + const { + instantSearchInstance, + renderFn, + hits, + } = createRenderedWidget(); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(1); // view event by render + + const { sendEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + sendEvent('conversion', hits[1], 'Product Ordered'); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledTimes(2); + expect( + instantSearchInstance.sendEventToInsights + ).toHaveBeenCalledWith({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); }); }); - }); - describe('bindEvent', () => { - it('returns a payload for click event', () => { - const { renderFn, hits } = createRenderedWidget(); - const { bindEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - const payload = bindEvent('click', hits[0], 'Product Added'); - expect(payload.startsWith('data-insights-event=')).toBe(true); - expect( - JSON.parse(atob(payload.substr('data-insights-event='.length))) - ).toEqual({ - eventType: 'click', - insightsMethod: 'clickedObjectIDsAfterSearch', - payload: { - eventName: 'Product Added', - index: '', - objectIDs: ['1'], - positions: [0], - queryID: 'test-query-id', - }, - widgetType: 'ais.infiniteHits', + describe('bindEvent', () => { + it('returns a payload for click event', () => { + const { renderFn, hits } = createRenderedWidget(); + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('click', hits[0], 'Product Added'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'click', + insightsMethod: 'clickedObjectIDsAfterSearch', + payload: { + eventName: 'Product Added', + index: '', + objectIDs: ['1'], + positions: [0], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); }); - }); - it('returns a payload for conversion event', () => { - const { renderFn, hits } = createRenderedWidget(); - const { bindEvent } = renderFn.mock.calls[ - renderFn.mock.calls.length - 1 - ][0]; - const payload = bindEvent('conversion', hits[1], 'Product Ordered'); - expect(payload.startsWith('data-insights-event=')).toBe(true); - expect( - JSON.parse(atob(payload.substr('data-insights-event='.length))) - ).toEqual({ - eventType: 'conversion', - insightsMethod: 'convertedObjectIDsAfterSearch', - payload: { - eventName: 'Product Ordered', - index: '', - objectIDs: ['2'], - queryID: 'test-query-id', - }, - widgetType: 'ais.infiniteHits', + it('returns a payload for conversion event', () => { + const { renderFn, hits } = createRenderedWidget(); + const { bindEvent } = renderFn.mock.calls[ + renderFn.mock.calls.length - 1 + ][0]; + const payload = bindEvent('conversion', hits[1], 'Product Ordered'); + expect(payload.startsWith('data-insights-event=')).toBe(true); + expect( + JSON.parse(atob(payload.substr('data-insights-event='.length))) + ).toEqual({ + eventType: 'conversion', + insightsMethod: 'convertedObjectIDsAfterSearch', + payload: { + eventName: 'Product Ordered', + index: '', + objectIDs: ['2'], + queryID: 'test-query-id', + }, + widgetType: 'ais.infiniteHits', + }); }); }); }); diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.js index 6bcc26dc34..10d51d0185 100644 --- a/src/connectors/menu/__tests__/connectMenu-test.js +++ b/src/connectors/menu/__tests__/connectMenu-test.js @@ -1088,7 +1088,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); }); - describe('sendEvent', () => { + describe('insights', () => { const createInitializedWidget = () => { const widget = makeWidget({ attribute: 'category', diff --git a/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts index 647d19ad34..79b8c63cdf 100644 --- a/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts +++ b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.ts @@ -1018,7 +1018,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/numeric-men }); }); - describe('sendEvent', () => { + describe('insights', () => { it('sends event when a facet is added', () => { const rendering = jest.fn(); const makeWidget = connectNumericMenu(rendering); diff --git a/src/connectors/range/__tests__/connectRange-test.js b/src/connectors/range/__tests__/connectRange-test.js index d51088a207..8b6c79eb34 100644 --- a/src/connectors/range/__tests__/connectRange-test.js +++ b/src/connectors/range/__tests__/connectRange-test.js @@ -1533,7 +1533,7 @@ describe('getWidgetSearchParameters', () => { expect(actual).toEqual(expectation); }); - describe('sendEvent', () => { + describe('insights', () => { it('sends event when a facet is added at each step', () => { rendering = jest.fn(); const makeWidget = connectRange(rendering); diff --git a/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js index fe040684a5..ed9c979cb3 100644 --- a/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js +++ b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js @@ -535,7 +535,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/rating-menu }); }); - describe('sendEvent', () => { + describe('insights', () => { it('sends event when a facet is added', () => { const attribute = 'swag'; const { refine, instantSearchInstance } = getInitializedWidget({ diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js index 479c0a019e..989623b29f 100644 --- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js +++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js @@ -2401,7 +2401,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- }); }); - describe('sendEvent', () => { + describe('insights', () => { const createInitializedWidget = () => { const factoryResult = createWidgetFactory(); const makeWidget = factoryResult.makeWidget; diff --git a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js index e7fcc844b9..2b5f9fdf9f 100644 --- a/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js +++ b/src/connectors/toggle-refinement/__tests__/connectToggleRefinement-test.js @@ -1092,7 +1092,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/toggle-refi }); }); - describe('sendEvent', () => { + describe('insights', () => { const createInitializedWidget = () => { const rendering = jest.fn(); const instantSearchInstance = createInstantSearch(); diff --git a/src/widgets/hits/__tests__/hits-integration-test.ts b/src/widgets/hits/__tests__/hits-integration-test.ts index 9bc041591a..cdee99a4c8 100644 --- a/src/widgets/hits/__tests__/hits-integration-test.ts +++ b/src/widgets/hits/__tests__/hits-integration-test.ts @@ -48,7 +48,7 @@ describe('hits', () => { container = document.createElement('div'); }); - describe('sendEvent', () => { + describe('insights', () => { const createInsightsMiddlewareWithOnEvent = () => { const onEvent = jest.fn(); const insights = createInsightsMiddleware({ diff --git a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts index 87b8848810..50639be096 100644 --- a/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts +++ b/src/widgets/infinite-hits/__tests__/infinite-hits-integration-test.ts @@ -154,7 +154,7 @@ describe('infiniteHits', () => { }); }); - describe('sendEvent', () => { + describe('insights', () => { const createInsightsMiddlewareWithOnEvent = () => { const onEvent = jest.fn(); const insights = createInsightsMiddleware({