From 7f50c46687abba6abb9e59fba56af34f27187eaf Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 18 Oct 2023 13:37:58 -0700 Subject: [PATCH] refactor(client-recommend): small refactors to recommendation filters --- .../src/Client/apis/Recommend.test.ts | 29 ++++++++++++ .../snap-client/src/Client/apis/Recommend.ts | 13 +++--- .../recommendationFiltersGet.test.ts | 42 +++++++++--------- .../transforms/recommendationFiltersGet.ts | 1 - .../recommendationFiltersPost.test.ts | 44 ++++++++++--------- .../transforms/recommendationFiltersPost.ts | 22 ++++++++-- packages/snap-client/src/types.ts | 6 +-- .../RecommendationController.ts | 4 -- .../e2e/recommendation/recommendation.cy.js | 40 ----------------- .../RecommendationInstantiator.test.tsx | 39 +++++++++++++++- .../RecommendationInstantiator.tsx | 7 ++- 11 files changed, 144 insertions(+), 103 deletions(-) diff --git a/packages/snap-client/src/Client/apis/Recommend.test.ts b/packages/snap-client/src/Client/apis/Recommend.test.ts index 1199922c8..2b598feda 100644 --- a/packages/snap-client/src/Client/apis/Recommend.test.ts +++ b/packages/snap-client/src/Client/apis/Recommend.test.ts @@ -286,6 +286,35 @@ describe('Recommend Api', () => { requestMock.mockReset(); }); + it('batchRecommendations handles filters expected', async () => { + const api = new RecommendAPI(new ApiConfiguration({})); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + // @ts-ignore + api.batchRecommendations({ + tags: ['crossSell'], + limits: 10, + filters: [ + { + type: 'value', + field: 'color', + value: 'red', + }, + ], + ...batchParams, + }); + + //add delay for paramBatch.timeout + await wait(250); + const reorderedGetURL = + 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend?tags=crossSell&limits=10&siteId=8uyt2m&lastViewed=marnie-runner-2-7x10&lastViewed=ruby-runner-2-7x10&lastViewed=abbie-runner-2-7x10&lastViewed=riley-4x6&lastViewed=joely-5x8&lastViewed=helena-4x6&lastViewed=kwame-4x6&lastViewed=sadie-4x6&lastViewed=candice-runner-2-7x10&lastViewed=esmeray-4x6&lastViewed=camilla-230x160&lastViewed=candice-4x6&lastViewed=sahara-4x6&lastViewed=dayna-4x6&lastViewed=moema-4x6&product=marnie-runner-2-7x10&filter.color=red'; + expect(requestMock).toHaveBeenCalledWith(reorderedGetURL, GETParams); + requestMock.mockReset(); + }); + it('batchRecommendations resolves in right order with order prop', async () => { const api = new RecommendAPI(new ApiConfiguration({})); const response = mockData.file('recommend/results/8uyt2m/ordered.json'); diff --git a/packages/snap-client/src/Client/apis/Recommend.ts b/packages/snap-client/src/Client/apis/Recommend.ts index 082e27e9e..5b61ea1ba 100644 --- a/packages/snap-client/src/Client/apis/Recommend.ts +++ b/packages/snap-client/src/Client/apis/Recommend.ts @@ -1,5 +1,5 @@ import { API, ApiConfiguration } from './Abstract'; -import { HTTPHeaders, filtersObj, PostRecommendationRequestFiltersModel, GetRecommendRequestModel } from '../../types'; +import { HTTPHeaders, PostRecommendRequestFiltersModel, PostRecommendRequestModel, GetRecommendRequestModel } from '../../types'; import { AppMode, charsParams } from '@searchspring/snap-toolbox'; import { transformRecommendationFiltersGet, transformRecommendationFiltersPost } from '../transforms'; import { ProfileRequestModel, ProfileResponseModel, RecommendRequestModel, RecommendResponseModel } from '../../types'; @@ -106,20 +106,19 @@ export class RecommendAPI extends API { } let response: RecommendResponseModel; - if (charsParams(batch.request) > 1024) { if (batch.request['product']) { batch.request['product'] = batch.request['product'].toString(); } - // //transform filters here + //transform filters here if (batch.request.filters) { - (batch.request as PostRecommendationRequestFiltersModel)['filters'] = transformRecommendationFiltersPost( + (batch.request as PostRecommendRequestModel)['filters'] = transformRecommendationFiltersPost( batch.request.filters - ) as filtersObj[]; + ) as PostRecommendRequestFiltersModel[]; } - response = await this.postRecommendations(batch.request as PostRecommendationRequestFiltersModel); + response = await this.postRecommendations(batch.request as PostRecommendRequestModel); } else { if (batch.request.filters) { const filters = transformRecommendationFiltersGet(batch.request.filters); @@ -167,7 +166,7 @@ export class RecommendAPI extends API { return response as unknown as RecommendResponseModel; } - async postRecommendations(requestParameters: PostRecommendationRequestFiltersModel): Promise { + async postRecommendations(requestParameters: PostRecommendRequestModel): Promise { const headerParameters: HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersGet.test.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersGet.test.ts index ca18eb2a7..9eee90a81 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersGet.test.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersGet.test.ts @@ -1,26 +1,28 @@ import { transformRecommendationFiltersGet } from './recommendationFiltersGet'; -it('transformFilters works with GET', async () => { - const filters = [ - { type: 'value' as const, field: 'color', value: 'blue' }, - { type: 'value' as const, field: 'color', value: 'red' }, - { type: 'value' as const, field: 'color', value: 'green' }, - { type: 'range' as const, field: 'price', value: { high: 20, low: 0 } }, - ]; - const tranformedFilters = transformRecommendationFiltersGet(filters); - expect(tranformedFilters).toStrictEqual({ 'filter.color': ['blue', 'red', 'green'], 'filter.price.high': [20], 'filter.price.low': [0] }); -}); +describe('transformRecommendationFiltersGet', () => { + it('transformRecommendationFiltersGet works', async () => { + const filters = [ + { type: 'value' as const, field: 'color', value: 'blue' }, + { type: 'value' as const, field: 'color', value: 'red' }, + { type: 'value' as const, field: 'color', value: 'green' }, + { type: 'range' as const, field: 'price', value: { high: 20, low: 0 } }, + ]; + const tranformedFilters = transformRecommendationFiltersGet(filters); + expect(tranformedFilters).toStrictEqual({ 'filter.color': ['blue', 'red', 'green'], 'filter.price.high': [20], 'filter.price.low': [0] }); + }); -it('transformFilters GET works with single high or low range value', async () => { - const filterHigh = [{ type: 'range' as const, field: 'price', value: { high: 20 } }]; - const filterLow = [ - //also works with 0 - { type: 'range' as const, field: 'price', value: { low: 0 } }, - ]; + it('transformRecommendationFiltersGet works with single high or low range value', async () => { + const filterHigh = [{ type: 'range' as const, field: 'price', value: { high: 20 } }]; + const filterLow = [ + //also works with 0 + { type: 'range' as const, field: 'price', value: { low: 0 } }, + ]; - const tranformedFilterHigh = transformRecommendationFiltersGet(filterHigh); - expect(tranformedFilterHigh).toStrictEqual({ 'filter.price.high': [20] }); + const tranformedFilterHigh = transformRecommendationFiltersGet(filterHigh); + expect(tranformedFilterHigh).toStrictEqual({ 'filter.price.high': [20] }); - const tranformedFilterLow = transformRecommendationFiltersGet(filterLow); - expect(tranformedFilterLow).toStrictEqual({ 'filter.price.low': [0] }); + const tranformedFilterLow = transformRecommendationFiltersGet(filterLow); + expect(tranformedFilterLow).toStrictEqual({ 'filter.price.low': [0] }); + }); }); diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersGet.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersGet.ts index e2a7606a6..8043c7e9c 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersGet.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersGet.ts @@ -9,7 +9,6 @@ export const transformRecommendationFiltersGet = (filters: RecommendationRequest if (filter.type == 'value') { //check if filterArray contains a filter for this value already if (filterArray[`filter.${filter.field}`]) { - // is the existing filter an array already? or just a single value filterArray[`filter.${filter.field}`].push(filter.value); } else { //else create a new one diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.test.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.test.ts index 9ab9797c2..153918d98 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.test.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.test.ts @@ -1,28 +1,30 @@ import { transformRecommendationFiltersPost } from './recommendationFiltersPost'; -it('transformFilters works with POST', async () => { - const filters = [ - { type: 'value' as const, field: 'color', value: 'blue' }, - { type: 'value' as const, field: 'color', value: 'red' }, - { type: 'value' as const, field: 'color', value: 'green' }, - { type: 'range' as const, field: 'price', value: { high: 20, low: 0 } }, - ]; +describe('transformRecommendationFiltersPost', () => { + it('transformRecommendationFiltersPost works as expected', async () => { + const filters = [ + { type: 'value' as const, field: 'color', value: 'blue' }, + { type: 'value' as const, field: 'color', value: 'red' }, + { type: 'value' as const, field: 'color', value: 'green' }, + { type: 'range' as const, field: 'price', value: { high: 20, low: 0 } }, + ]; - const tranformedFilters = transformRecommendationFiltersPost(filters); - expect(tranformedFilters).toStrictEqual([ - { field: 'color', type: '=', values: ['blue', 'red', 'green'] }, - { field: 'price', type: '>=', values: [0] }, - { field: 'price', type: '<=', values: [20] }, - ]); -}); + const tranformedFilters = transformRecommendationFiltersPost(filters); + expect(tranformedFilters).toStrictEqual([ + { field: 'color', type: '=', values: ['blue', 'red', 'green'] }, + { field: 'price', type: '>=', values: [0] }, + { field: 'price', type: '<=', values: [20] }, + ]); + }); -it('transformFilters POST works with single high or low range value', async () => { - const filterHigh = [{ type: 'range' as const, field: 'price', value: { high: 20 } }]; - const filterLow = [{ type: 'range' as const, field: 'price', value: { low: 0 } }]; + it('transformRecommendationFiltersPost works with single high or low range value', async () => { + const filterHigh = [{ type: 'range' as const, field: 'price', value: { high: 20 } }]; + const filterLow = [{ type: 'range' as const, field: 'price', value: { low: 0 } }]; - const tranformedFilterHigh = transformRecommendationFiltersPost(filterHigh); - expect(tranformedFilterHigh).toStrictEqual([{ field: 'price', type: '<=', values: [20] }]); + const tranformedFilterHigh = transformRecommendationFiltersPost(filterHigh); + expect(tranformedFilterHigh).toStrictEqual([{ field: 'price', type: '<=', values: [20] }]); - const tranformedFilterLow = transformRecommendationFiltersPost(filterLow); - expect(tranformedFilterLow).toStrictEqual([{ field: 'price', type: '>=', values: [0] }]); + const tranformedFilterLow = transformRecommendationFiltersPost(filterLow); + expect(tranformedFilterLow).toStrictEqual([{ field: 'price', type: '>=', values: [0] }]); + }); }); diff --git a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts index f83b06244..6b04e447f 100644 --- a/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts +++ b/packages/snap-client/src/Client/transforms/recommendationFiltersPost.ts @@ -1,7 +1,7 @@ -import { RecommendationRequestFilterModel, filtersObj } from '../../types'; +import { RecommendationRequestFilterModel, PostRecommendRequestFiltersModel } from '../../types'; export const transformRecommendationFiltersPost = (filters: RecommendationRequestFilterModel[]) => { - const filterArray: filtersObj[] = []; + const filterArray: PostRecommendRequestFiltersModel[] = []; filters.map((filter) => { if (filter.type == 'value') { //check if filterArray contains a filter for this value already @@ -27,7 +27,14 @@ export const transformRecommendationFiltersPost = (filters: RecommendationReques type: '>=' as const, values: [filter.value.low as number], }; - filterArray.push(low); + + //dedupe + const i = filterArray.findIndex((_filter) => _filter.field == filter.field && _filter.type == '>='); + if (i > -1) { + filterArray[i] = low; + } else { + filterArray.push(low); + } } //high if (typeof filter.value.high == 'number') { @@ -36,7 +43,14 @@ export const transformRecommendationFiltersPost = (filters: RecommendationReques type: '<=' as const, values: [filter.value.high as number], }; - filterArray.push(high); + + //dedupe + const i = filterArray.findIndex((_filter) => _filter.field == filter.field && _filter.type == '<='); + if (i > -1) { + filterArray[i] = high; + } else { + filterArray.push(high); + } } } }); diff --git a/packages/snap-client/src/types.ts b/packages/snap-client/src/types.ts index 8f94eff04..8a8da3fae 100644 --- a/packages/snap-client/src/types.ts +++ b/packages/snap-client/src/types.ts @@ -114,11 +114,11 @@ export type GetRecommendRequestModel = Omit & [filter: `filter.${string}`]: (string | number)[]; }; -export type PostRecommendationRequestFiltersModel = Omit & { - filters?: filtersObj[]; +export type PostRecommendRequestModel = Omit & { + filters?: PostRecommendRequestFiltersModel[]; }; -export type filtersObj = { +export type PostRecommendRequestFiltersModel = { field: string; type: '=' | '==' | '===' | '!=' | '!==' | '>' | '<' | '>=' | '<='; values: (string | number)[]; diff --git a/packages/snap-controller/src/Recommendation/RecommendationController.ts b/packages/snap-controller/src/Recommendation/RecommendationController.ts index 3ab1f5455..7d94394e8 100644 --- a/packages/snap-controller/src/Recommendation/RecommendationController.ts +++ b/packages/snap-controller/src/Recommendation/RecommendationController.ts @@ -299,10 +299,6 @@ export class RecommendationController extends AbstractController { const cart = this.tracker.cookies.cart.get(); const lastViewed = this.tracker.cookies.viewed.get(); - if (this.context.options?.filters) { - params.filters = this.context.options?.filters; - } - if (shopperId) { params.shopper = shopperId; } diff --git a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js index ed19332e2..9ae7b45ad 100644 --- a/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js +++ b/packages/snap-preact-demo/tests/cypress/e2e/recommendation/recommendation.cy.js @@ -133,45 +133,5 @@ describe('Recommendations', () => { }); }); }); - - it('adds filters to store', function () { - const filters = [ - { - type: 'value', - field: 'color', - value: 'blue', - }, - { - type: 'range', - field: 'price', - value: { low: 0, high: 20 }, - }, - ]; - cy.on('window:before:load', (win) => { - win.mergeSnapConfig = { - instantiators: { - recommendation: { - config: { - globals: { - filters: filters, - }, - }, - }, - }, - }; - }); - - cy.visit(config.url); - - it('snap bundle exists on product page', () => { - cy.waitForBundle().then((searchspring) => { - expect(searchspring).to.exist; - }); - }); - - cy.snapController(config?.selectors?.recommendation.controller).then(({ store }) => { - expect(store.config.globals.filters).to.deep.equal(filters); - }); - }); }); }); diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx index d61c6de28..ab4902a47 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.test.tsx @@ -284,8 +284,21 @@ describe('RecommendationInstantiator', () => { branch: 'testing', siteId: 'abc123', categories: ['cats', 'dogs'], - limit: 5 + limit: 5, + filters: [ + { + type: 'value', + field: 'color', + value: 'blue' + }, + { + type: 'range', + field: 'price', + value: { low: 0, high: 20 } + } + ] } + `; const client = new MockClient(baseConfig.client!.globals, {}); @@ -305,6 +318,18 @@ describe('RecommendationInstantiator', () => { options: { branch: 'testing', categories: ['cats', 'dogs'], + filters: [ + { + type: 'value', + field: 'color', + value: 'blue', + }, + { + type: 'range', + field: 'price', + value: { low: 0, high: 20 }, + }, + ], limit: 5, siteId: 'abc123', }, @@ -316,6 +341,18 @@ describe('RecommendationInstantiator', () => { batched: true, branch: 'testing', categories: ['cats', 'dogs'], + filters: [ + { + type: 'value', + field: 'color', + value: 'blue', + }, + { + type: 'range', + field: 'price', + value: { low: 0, high: 20 }, + }, + ], limits: 5, product: 'sku1', shopper: 'snapdev', diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx index ab896c2c8..c3d2123fc 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx @@ -166,6 +166,9 @@ export class RecommendationInstantiator { if (options?.categories) { contextGlobals.categories = options.categories; } + if (options?.filters) { + contextGlobals.filters = options.filters; + } if (options?.limit && Number.isInteger(Number(options?.limit))) { contextGlobals.limits = Number(options?.limit); } @@ -193,8 +196,8 @@ export class RecommendationInstantiator { limits: 20, }; const globals = deepmerge( - deepmerge(deepmerge(defaultGlobals, this.config.client?.globals || {}), contextGlobals), - (this.config.config?.globals as any) || {} + deepmerge(deepmerge(defaultGlobals, this.config.client?.globals || {}), (this.config.config?.globals as any) || {}), + contextGlobals ); const controllerConfig = {