From a24804a0ab9c6daecff7bcb2890896a0569f8608 Mon Sep 17 00:00:00 2001 From: Nicholas-David Labarre Date: Thu, 24 Aug 2023 10:33:39 -0400 Subject: [PATCH] feat(productlistings): create plpv2 interactive controller [CAPI-85] --- ...duct-listing-v2-interactive-result.test.ts | 82 +++++++++++++++++++ .../product-listing-v2-interactive-result.ts | 58 +++++++++++++ packages/headless/src/test/mock-engine.ts | 20 +++++ 3 files changed, 160 insertions(+) create mode 100644 packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.test.ts create mode 100644 packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.ts diff --git a/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.test.ts b/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.test.ts new file mode 100644 index 00000000000..89b2f0aa194 --- /dev/null +++ b/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.test.ts @@ -0,0 +1,82 @@ +import {ProductRecommendation} from '../../../../api/search/search/product-recommendation'; +import {configuration} from '../../../../app/common-reducers'; +import { + buildMockCommerceEngine, + MockCommerceEngine, +} from '../../../../test/mock-engine'; +import {buildMockProductRecommendation} from '../../../../test/mock-product-recommendation'; +import {logProductRecommendationOpen} from '../../product-listing-analytics'; +import {pushRecentResult} from '../../product-listing-recent-results'; +import { + buildInteractiveResult, + InteractiveResult, +} from './product-listing-v2-interactive-result'; + +describe('InteractiveResult', () => { + let engine: MockCommerceEngine; + let mockProductRec: ProductRecommendation; + let interactiveResult: InteractiveResult; + let logDocumentOpenPendingActionType: string; + + const productRecStringParams = { + permanentid: 'permanentid', + documentUri: 'documentUri', + clickUri: 'clickUri', + }; + + function initializeInteractiveResult(delay?: number) { + const productRec = (mockProductRec = buildMockProductRecommendation( + productRecStringParams + )); + logDocumentOpenPendingActionType = + logProductRecommendationOpen(mockProductRec).pending.type; + interactiveResult = buildInteractiveResult(engine, { + options: {result: productRec, selectionDelay: delay}, + }); + } + + function findLogDocumentAction() { + return ( + engine.actions.find( + (action) => action.type === logDocumentOpenPendingActionType + ) ?? null + ); + } + + function expectLogDocumentActionPending() { + const action = findLogDocumentAction(); + expect(action).toEqual( + logProductRecommendationOpen(mockProductRec).pending( + action!.meta.requestId + ) + ); + } + + beforeEach(() => { + engine = buildMockCommerceEngine(); + initializeInteractiveResult(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('adds the correct reducers to engine', () => { + expect(engine.addReducers).toHaveBeenCalledWith({configuration}); + }); + + it('when calling select() should add the result to recent results list', () => { + interactiveResult.select(); + jest.runAllTimers(); + + expect( + engine.actions.find((a) => a.type === pushRecentResult.type) + ).toBeDefined(); + }); + + it('when calling select(), logs documentOpen', () => { + interactiveResult.select(); + expectLogDocumentActionPending(); + }); +}); diff --git a/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.ts b/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.ts new file mode 100644 index 00000000000..9717dfd15af --- /dev/null +++ b/packages/headless/src/features/product-listing/v2/result-list/product-listing-v2-interactive-result.ts @@ -0,0 +1,58 @@ +import {ProductRecommendation} from '../../../../api/search/search/product-recommendation'; +import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine'; +import { + InteractiveResultCore, + InteractiveResultCoreOptions, + InteractiveResultCoreProps, +} from '../../../../controllers'; +import {buildInteractiveResultCore} from '../../../../controllers/core/interactive-result/headless-core-interactive-result'; +import {logProductRecommendationOpen} from '../../product-listing-analytics'; +import {pushRecentResult} from '../../product-listing-recent-results'; + +export interface InteractiveResultOptions extends InteractiveResultCoreOptions { + /** + * The query result. + */ + result: ProductRecommendation; +} + +export interface InteractiveResultProps extends InteractiveResultCoreProps { + /** + * The options for the `InteractiveResult` controller. + * */ + options: InteractiveResultOptions; +} + +/** + * The `InteractiveResult` controller provides an interface for triggering desirable side effects, such as logging UA events to the Coveo Platform, when a user selects a query result. + */ +export interface InteractiveResult extends InteractiveResultCore {} + +/** + * Creates an `InteractiveResult` controller instance. + * + * @param engine - The headless engine. + * @param props - The configurable `InteractiveResult` properties. + * @returns An `InteractiveResult` controller instance. + */ +export function buildInteractiveResult( + engine: CommerceEngine, + props: InteractiveResultProps +): InteractiveResult { + let wasOpened = false; + + const logAnalyticsIfNeverOpened = () => { + if (wasOpened) { + return; + } + wasOpened = true; + engine.dispatch(logProductRecommendationOpen(props.options.result)); + }; + + const action = () => { + logAnalyticsIfNeverOpened(); + engine.dispatch(pushRecentResult(props.options.result)); + }; + + return buildInteractiveResultCore(engine, props, action); +} diff --git a/packages/headless/src/test/mock-engine.ts b/packages/headless/src/test/mock-engine.ts index d442dec1a8e..ced97779d05 100644 --- a/packages/headless/src/test/mock-engine.ts +++ b/packages/headless/src/test/mock-engine.ts @@ -10,6 +10,7 @@ import thunk from 'redux-thunk'; import {SearchAPIClient} from '../api/search/search-api-client'; import {InsightAPIClient} from '../api/service/insight/insight-api-client'; import {analyticsMiddleware} from '../app/analytics-middleware'; +import {CommerceEngine} from '../app/commerce-engine/commerce-engine'; import {CoreEngine} from '../app/engine'; import {InsightEngine} from '../app/insight-engine/insight-engine'; import {InsightThunkExtraArguments} from '../app/insight-thunk-extra-arguments'; @@ -25,6 +26,7 @@ import {SearchEngine} from '../app/search-engine/search-engine'; import {SearchThunkExtraArguments} from '../app/search-thunk-extra-arguments'; import {CaseAssistEngine} from '../case-assist.index'; import {CaseAssistAppState} from '../state/case-assist-app-state'; +import {ProductListingV2AppState} from '../state/commerce-app-state'; import {InsightAppState} from '../state/insight-app-state'; import {ProductListingAppState} from '../state/product-listing-app-state'; import {ProductRecommendationsAppState} from '../state/product-recommendations-app-state'; @@ -35,6 +37,7 @@ import {buildMockCaseAssistState} from './mock-case-assist-state'; import {buildMockInsightAPIClient} from './mock-insight-api-client'; import {buildMockInsightState} from './mock-insight-state'; import {buildMockProductListingState} from './mock-product-listing-state'; +import {buildMockProductListingV2State} from './mock-product-listing-v2-state'; import {buildMockProductRecommendationsState} from './mock-product-recommendations-state'; import {createMockRecommendationState} from './mock-recommendation-state'; import {buildMockSearchAPIClient} from './mock-search-api-client'; @@ -55,6 +58,7 @@ type AppState = | RecommendationAppState | ProductRecommendationsAppState | ProductListingAppState + | ProductListingV2AppState | CaseAssistAppState | InsightAppState; @@ -142,6 +146,22 @@ export function buildMockProductListingEngine( return buildMockCoreEngine(config, buildMockProductListingState); } +export interface MockCommerceEngine + extends CommerceEngine, + MockEngine {} + +/** + * For internal use only. + * + * Returns a non-functional `CommerceEngine`. + * To be used only for unit testing controllers, not for functional tests. + */ +export function buildMockCommerceEngine( + config: Partial> = {} +): MockCommerceEngine { + return buildMockCoreEngine(config, buildMockProductListingV2State); +} + export interface MockCaseAssistEngine extends CaseAssistEngine, MockEngine {}