From 2432693a597e89fb128bb1a3ddb71c07a469fceb Mon Sep 17 00:00:00 2001 From: Nicholas Labarre Date: Fri, 17 Nov 2023 15:47:49 -0500 Subject: [PATCH] feat(commerce): create very basic search controller, actions and slice (#3400) * create very basic search controller * simplify mock builder definition * apply review suggestions * normalize type name --- .../api/commerce/commerce-api-client.test.ts | 187 ++++++++++-------- .../src/api/commerce/commerce-api-client.ts | 38 ++-- .../src/api/commerce/commerce-api-params.ts | 10 +- .../v2 => common}/pagination.ts | 0 .../request.ts} | 41 ++-- .../src/api/commerce/common/response.ts | 21 ++ .../{product-listings/v2 => common}/sort.ts | 2 +- .../v2/product-listing-v2-response.ts | 21 -- .../src/api/commerce/search/request.ts | 4 + .../app/commerce-engine/commerce-engine.ts | 4 + packages/headless/src/commerce.index.ts | 3 + .../commerce/search/headless-search.test.ts | 39 ++++ .../commerce/search/headless-search.ts | 75 +++++++ .../src/features/commerce/common/actions.ts | 95 +++++++++ .../product-listing-actions-loader.ts | 12 +- .../product-listing-actions.ts | 78 +------- .../product-listing-slice.test.ts | 14 +- .../features/commerce/query/query-actions.ts | 18 ++ .../commerce/query/query-slice.test.ts | 37 ++++ .../features/commerce/query/query-slice.ts | 14 ++ .../features/commerce/query/query-state.ts | 7 + .../commerce/search/search-actions.ts | 42 ++++ .../commerce/search/search-slice.test.ts | 91 +++++++++ .../features/commerce/search/search-slice.ts | 25 +++ .../features/commerce/search/search-state.ts | 19 ++ .../src/features/commerce/sort/sort-slice.ts | 2 +- .../src/integration-tests/commerce.test.ts | 15 ++ .../headless/src/state/commerce-app-state.ts | 4 + packages/headless/src/state/state-sections.ts | 16 ++ .../headless/src/test/mock-commerce-search.ts | 3 + .../headless/src/test/mock-commerce-state.ts | 4 + .../src/test/mock-product-listing-v2.ts | 8 +- 32 files changed, 723 insertions(+), 226 deletions(-) rename packages/headless/src/api/commerce/{product-listings/v2 => common}/pagination.ts (100%) rename packages/headless/src/api/commerce/{product-listings/v2/product-listing-v2-request.ts => common/request.ts} (51%) create mode 100644 packages/headless/src/api/commerce/common/response.ts rename packages/headless/src/api/commerce/{product-listings/v2 => common}/sort.ts (77%) delete mode 100644 packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-response.ts create mode 100644 packages/headless/src/api/commerce/search/request.ts create mode 100644 packages/headless/src/controllers/commerce/search/headless-search.test.ts create mode 100644 packages/headless/src/controllers/commerce/search/headless-search.ts create mode 100644 packages/headless/src/features/commerce/common/actions.ts create mode 100644 packages/headless/src/features/commerce/query/query-actions.ts create mode 100644 packages/headless/src/features/commerce/query/query-slice.test.ts create mode 100644 packages/headless/src/features/commerce/query/query-slice.ts create mode 100644 packages/headless/src/features/commerce/query/query-state.ts create mode 100644 packages/headless/src/features/commerce/search/search-actions.ts create mode 100644 packages/headless/src/features/commerce/search/search-slice.test.ts create mode 100644 packages/headless/src/features/commerce/search/search-slice.ts create mode 100644 packages/headless/src/features/commerce/search/search-state.ts create mode 100644 packages/headless/src/test/mock-commerce-search.ts diff --git a/packages/headless/src/api/commerce/commerce-api-client.test.ts b/packages/headless/src/api/commerce/commerce-api-client.test.ts index 60a176336bb..ad3c43ecec3 100644 --- a/packages/headless/src/api/commerce/commerce-api-client.test.ts +++ b/packages/headless/src/api/commerce/commerce-api-client.test.ts @@ -2,8 +2,8 @@ import {SortBy} from '../../features/sort/sort'; import {buildMockCommerceAPIClient} from '../../test/mock-commerce-api-client'; import {PlatformClient} from '../platform-client'; import {CommerceAPIClient} from './commerce-api-client'; -import {ProductListingV2Request} from './product-listings/v2/product-listing-v2-request'; -import {ProductListingV2} from './product-listings/v2/product-listing-v2-response'; +import {CommerceAPIRequest} from './common/request'; +import {CommerceResponse} from './common/response'; describe('commerce api client', () => { const platformUrl = 'https://platformdev.cloud.coveo.com'; @@ -29,94 +29,123 @@ describe('commerce api client', () => { PlatformClient.call = platformCallMock; }; - describe('getProductListing', () => { - const buildGetListingV2Request = ( - req: Partial = {} - ): ProductListingV2Request => ({ - accessToken: accessToken, - organizationId: organizationId, - url: platformUrl, - trackingId: trackingId, - language: req.language ?? '', - currency: req.currency ?? '', - clientId: req.clientId ?? '', - context: req.context ?? { - view: {url: ''}, + const buildCommerceAPIRequest = ( + req: Partial = {} + ): CommerceAPIRequest => ({ + accessToken: accessToken, + organizationId: organizationId, + url: platformUrl, + trackingId: trackingId, + language: req.language ?? '', + currency: req.currency ?? '', + clientId: req.clientId ?? '', + context: req.context ?? { + view: {url: ''}, + }, + }); + + it('#getProductListing should call the platform endpoint with the correct arguments', async () => { + const request = buildCommerceAPIRequest(); + + mockPlatformCall({ + ok: true, + json: () => Promise.resolve('some content'), + }); + + await client.getProductListing(request); + + expect(platformCallMock).toBeCalled(); + const mockRequest = platformCallMock.mock.calls[0][0]; + expect(mockRequest).toMatchObject({ + method: 'POST', + contentType: 'application/json', + url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`, + accessToken: request.accessToken, + origin: 'commerceApiFetch', + requestParams: { + clientId: request.clientId, + context: request.context, + language: request.language, + currency: request.currency, }, }); + }); + + it('#search should call the platform endpoint with the correct arguments', async () => { + const request = { + ...buildCommerceAPIRequest(), + query: 'some query', + }; + + mockPlatformCall({ + ok: true, + json: () => Promise.resolve('some content'), + }); - it('should call the platform endpoint with the correct arguments', async () => { - const request = buildGetListingV2Request(); - - mockPlatformCall({ - ok: true, - json: () => Promise.resolve('some content'), - }); - - await client.getProductListing(request); - - expect(platformCallMock).toBeCalled(); - const mockRequest = platformCallMock.mock.calls[0][0]; - expect(mockRequest).toMatchObject({ - method: 'POST', - contentType: 'application/json', - url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`, - accessToken: request.accessToken, - origin: 'commerceApiFetch', - requestParams: { - clientId: request.clientId, - context: request.context, - language: request.language, - currency: request.currency, - }, - }); + await client.search(request); + + expect(platformCallMock).toBeCalled(); + const mockRequest = platformCallMock.mock.calls[0][0]; + expect(mockRequest).toMatchObject({ + method: 'POST', + contentType: 'application/json', + url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/search`, + accessToken: request.accessToken, + origin: 'commerceApiFetch', + requestParams: { + query: 'some query', + clientId: request.clientId, + context: request.context, + language: request.language, + currency: request.currency, + }, }); + }); - it('should return error response on failure', async () => { - const request = buildGetListingV2Request(); + it('should return error response on failure', async () => { + const request = buildCommerceAPIRequest(); - const expectedError = { - statusCode: 401, - message: 'Unauthorized', - type: 'authorization', - }; + const expectedError = { + statusCode: 401, + message: 'Unauthorized', + type: 'authorization', + }; - mockPlatformCall({ - ok: false, - json: () => Promise.resolve(expectedError), - }); + mockPlatformCall({ + ok: false, + json: () => Promise.resolve(expectedError), + }); - const response = await client.getProductListing(request); + const response = await client.getProductListing(request); - expect(response).toMatchObject({ - error: expectedError, - }); + expect(response).toMatchObject({ + error: expectedError, }); + }); + + it('should return success response on success', async () => { + const request = buildCommerceAPIRequest(); + + const expectedBody: CommerceResponse = { + products: [], + facets: [], + pagination: {page: 0, perPage: 0, totalCount: 0, totalPages: 0}, + responseId: '', + sort: { + appliedSort: {sortCriteria: SortBy.Relevance}, + availableSorts: [{sortCriteria: SortBy.Relevance}], + }, + }; + + mockPlatformCall({ + ok: true, + json: () => Promise.resolve(expectedBody), + }); + + const response = await client.getProductListing(request); - it('should return success response on success', async () => { - const request = buildGetListingV2Request(); - - const expectedBody: ProductListingV2 = { - products: [], - facets: [], - pagination: {page: 0, perPage: 0, totalCount: 0, totalPages: 0}, - responseId: '', - sort: { - appliedSort: {sortCriteria: SortBy.Relevance}, - availableSorts: [{sortCriteria: SortBy.Relevance}], - }, - }; - - mockPlatformCall({ - ok: true, - json: () => Promise.resolve(expectedBody), - }); - - const response = await client.getProductListing(request); - - expect(response).toMatchObject({ - success: expectedBody, - }); + expect(response).toMatchObject({ + success: expectedBody, }); }); }); diff --git a/packages/headless/src/api/commerce/commerce-api-client.ts b/packages/headless/src/api/commerce/commerce-api-client.ts index decc01dcc0f..13bba9904ce 100644 --- a/packages/headless/src/api/commerce/commerce-api-client.ts +++ b/packages/headless/src/api/commerce/commerce-api-client.ts @@ -1,18 +1,16 @@ import {Logger} from 'pino'; import {CommerceThunkExtraArguments} from '../../app/commerce-thunk-extra-arguments'; import {CommerceAppState} from '../../state/commerce-app-state'; -import {PlatformClient} from '../platform-client'; +import {PlatformClient, PlatformClientCallOptions} from '../platform-client'; import {PreprocessRequest} from '../preprocess-request'; import {buildAPIResponseFromErrorOrThrow} from '../search/search-api-error-response'; import { CommerceAPIErrorResponse, CommerceAPIErrorStatusResponse, } from './commerce-api-error-response'; -import { - buildProductListingV2Request, - ProductListingV2Request, -} from './product-listings/v2/product-listing-v2-request'; -import {ProductListingV2SuccessResponse} from './product-listings/v2/product-listing-v2-response'; +import {buildRequest, CommerceAPIRequest} from './common/request'; +import {CommerceSuccessResponse} from './common/response'; +import {CommerceSearchRequest} from './search/request'; export interface AsyncThunkCommerceOptions< T extends Partial, @@ -39,12 +37,30 @@ export class CommerceAPIClient { constructor(private options: CommerceAPIClientOptions) {} async getProductListing( - req: ProductListingV2Request - ): Promise> { - const response = await PlatformClient.call({ - ...buildProductListingV2Request(req), + req: CommerceAPIRequest + ): Promise> { + return this.query({ + ...buildRequest(req, 'listing'), ...this.options, }); + } + + async search( + req: CommerceSearchRequest + ): Promise> { + const requestOptions = buildRequest(req, 'search'); + return this.query({ + ...requestOptions, + requestParams: { + ...requestOptions.requestParams, + query: req?.query, + }, + ...this.options, + }); + } + + private async query(options: PlatformClientCallOptions) { + const response = await PlatformClient.call(options); if (response instanceof Error) { return buildAPIResponseFromErrorOrThrow(response); @@ -52,7 +68,7 @@ export class CommerceAPIClient { const body = await response.json(); return response.ok - ? {success: body as ProductListingV2SuccessResponse} + ? {success: body as CommerceSuccessResponse} : {error: body as CommerceAPIErrorStatusResponse}; } } diff --git a/packages/headless/src/api/commerce/commerce-api-params.ts b/packages/headless/src/api/commerce/commerce-api-params.ts index d2fba8d5fec..3be1d7451f1 100644 --- a/packages/headless/src/api/commerce/commerce-api-params.ts +++ b/packages/headless/src/api/commerce/commerce-api-params.ts @@ -1,5 +1,5 @@ import {CommerceFacetRequest} from '../../features/commerce/facets/facet-set/interfaces/request'; -import {SortOption} from './product-listings/v2/sort'; +import {SortOption} from './common/sort'; export interface TrackingIdParam { trackingId: string; @@ -60,10 +60,14 @@ export interface FacetsParam { facets?: CommerceFacetRequest[]; } -export interface SelectedPageParam { +export interface PageParam { page?: number; } -export interface SelectedSortParam { +export interface SortParam { sort?: SortOption; } + +export interface QueryParam { + query?: string; +} diff --git a/packages/headless/src/api/commerce/product-listings/v2/pagination.ts b/packages/headless/src/api/commerce/common/pagination.ts similarity index 100% rename from packages/headless/src/api/commerce/product-listings/v2/pagination.ts rename to packages/headless/src/api/commerce/common/pagination.ts diff --git a/packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-request.ts b/packages/headless/src/api/commerce/common/request.ts similarity index 51% rename from packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-request.ts rename to packages/headless/src/api/commerce/common/request.ts index 005282e261c..e72f86fe83d 100644 --- a/packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-request.ts +++ b/packages/headless/src/api/commerce/common/request.ts @@ -1,38 +1,34 @@ -import { - HTTPContentType, - HttpMethods, - PlatformClientCallOptions, -} from '../../../platform-client'; -import {BaseParam} from '../../../platform-service-params'; +import {PlatformClientCallOptions} from '../../platform-client'; +import {BaseParam} from '../../platform-service-params'; import { ClientIdParam, ContextParam, - LanguageParam, CurrencyParam, + LanguageParam, FacetsParam, - SelectedPageParam, - SelectedSortParam, + PageParam, + SortParam, TrackingIdParam, -} from '../../commerce-api-params'; +} from '../commerce-api-params'; -export type ProductListingV2Request = BaseParam & +export type CommerceAPIRequest = BaseParam & TrackingIdParam & LanguageParam & CurrencyParam & ClientIdParam & ContextParam & FacetsParam & - SelectedPageParam & - SelectedSortParam; + PageParam & + SortParam; -export const buildProductListingV2Request = (req: ProductListingV2Request) => { +export const buildRequest = (req: CommerceAPIRequest, path: string) => { return { - ...baseProductListingV2Request(req, 'POST', 'application/json'), + ...baseRequest(req, path), requestParams: prepareRequestParams(req), }; }; -const prepareRequestParams = (req: ProductListingV2Request) => { +const prepareRequestParams = (req: CommerceAPIRequest) => { const {clientId, context, language, currency, page, facets, sort} = req; return { clientId, @@ -45,21 +41,20 @@ const prepareRequestParams = (req: ProductListingV2Request) => { }; }; -export const baseProductListingV2Request = ( - req: ProductListingV2Request, - method: HttpMethods, - contentType: HTTPContentType +export const baseRequest = ( + req: CommerceAPIRequest, + path: string ): Pick< PlatformClientCallOptions, 'accessToken' | 'method' | 'contentType' | 'url' | 'origin' > => { const {url, organizationId, accessToken, trackingId} = req; - const baseUrl = `${url}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`; + const baseUrl = `${url}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/${path}`; return { accessToken, - method, - contentType, + method: 'POST', + contentType: 'application/json', url: baseUrl, origin: 'commerceApiFetch', }; diff --git a/packages/headless/src/api/commerce/common/response.ts b/packages/headless/src/api/commerce/common/response.ts new file mode 100644 index 00000000000..4f3d1f14cc4 --- /dev/null +++ b/packages/headless/src/api/commerce/common/response.ts @@ -0,0 +1,21 @@ +import {AnyFacetResponse} from '../../../features/commerce/facets/facet-set/interfaces/response'; +import { + SearchAPIErrorWithExceptionInBody, + SearchAPIErrorWithStatusCode, +} from '../../search/search-api-error-response'; +import {ProductRecommendation} from '../../search/search/product-recommendation'; +import {Pagination} from './pagination'; +import {Sort} from './sort'; + +export interface CommerceSuccessResponse { + responseId: string; + products: ProductRecommendation[]; + facets: AnyFacetResponse[]; + pagination: Pagination; + sort: Sort; +} + +export type CommerceResponse = + | CommerceSuccessResponse + | SearchAPIErrorWithExceptionInBody + | SearchAPIErrorWithStatusCode; diff --git a/packages/headless/src/api/commerce/product-listings/v2/sort.ts b/packages/headless/src/api/commerce/common/sort.ts similarity index 77% rename from packages/headless/src/api/commerce/product-listings/v2/sort.ts rename to packages/headless/src/api/commerce/common/sort.ts index 461d3e04007..6895f7634a6 100644 --- a/packages/headless/src/api/commerce/product-listings/v2/sort.ts +++ b/packages/headless/src/api/commerce/common/sort.ts @@ -1,4 +1,4 @@ -import {SortBy, SortDirection} from '../../../../features/sort/sort'; +import {SortBy, SortDirection} from '../../../features/sort/sort'; export type SortOption = {sortCriteria: SortBy} & { fields?: { diff --git a/packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-response.ts b/packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-response.ts deleted file mode 100644 index 42259d6ec74..00000000000 --- a/packages/headless/src/api/commerce/product-listings/v2/product-listing-v2-response.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {AnyFacetResponse} from '../../../../features/commerce/facets/facet-set/interfaces/response'; -import { - SearchAPIErrorWithExceptionInBody, - SearchAPIErrorWithStatusCode, -} from '../../../search/search-api-error-response'; -import {ProductRecommendation} from '../../../search/search/product-recommendation'; -import {Pagination} from './pagination'; -import {Sort} from './sort'; - -export interface ProductListingV2SuccessResponse { - responseId: string; - products: ProductRecommendation[]; - facets: AnyFacetResponse[]; - pagination: Pagination; - sort: Sort; -} - -export type ProductListingV2 = - | ProductListingV2SuccessResponse - | SearchAPIErrorWithExceptionInBody - | SearchAPIErrorWithStatusCode; diff --git a/packages/headless/src/api/commerce/search/request.ts b/packages/headless/src/api/commerce/search/request.ts new file mode 100644 index 00000000000..83fc01797fa --- /dev/null +++ b/packages/headless/src/api/commerce/search/request.ts @@ -0,0 +1,4 @@ +import {QueryParam} from '../commerce-api-params'; +import {CommerceAPIRequest} from '../common/request'; + +export type CommerceSearchRequest = CommerceAPIRequest & QueryParam; diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ts index d63a617eb50..a88aaa7cafb 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ts @@ -7,6 +7,8 @@ import {contextReducer} from '../../features/commerce/context/context-slice'; import {commerceFacetSetReducer} from '../../features/commerce/facets/facet-set/facet-set-slice'; import {paginationReducer} from '../../features/commerce/pagination/pagination-slice'; import {productListingV2Reducer} from '../../features/commerce/product-listing/product-listing-slice'; +import {queryReducer} from '../../features/commerce/query/query-slice'; +import {commerceSearchReducer} from '../../features/commerce/search/search-slice'; import {sortReducer} from '../../features/commerce/sort/sort-slice'; import {facetOrderReducer} from '../../features/facets/facet-order/facet-order-slice'; import {CommerceAppState} from '../../state/commerce-app-state'; @@ -28,11 +30,13 @@ export type {CommerceEngineConfiguration}; const commerceEngineReducers = { productListing: productListingV2Reducer, + commerceSearch: commerceSearchReducer, commercePagination: paginationReducer, commerceSort: sortReducer, facetOrder: facetOrderReducer, commerceFacetSet: commerceFacetSetReducer, commerceContext: contextReducer, + commerceQuery: queryReducer, cart: cartReducer, }; type CommerceEngineReducers = typeof commerceEngineReducers; diff --git a/packages/headless/src/commerce.index.ts b/packages/headless/src/commerce.index.ts index fc49ef2371c..e56802c55a9 100644 --- a/packages/headless/src/commerce.index.ts +++ b/packages/headless/src/commerce.index.ts @@ -107,3 +107,6 @@ export type { export {buildProductListingFacet} from './controllers/commerce/product-listing/facets/headless-product-listing-facet'; export {buildProductListingFacetGenerator} from './controllers/commerce/product-listing/facets/headless-product-listing-facet-generator'; + +export type {Search} from './controllers/commerce/search/headless-search'; +export {buildSearch} from './controllers/commerce/search/headless-search'; diff --git a/packages/headless/src/controllers/commerce/search/headless-search.test.ts b/packages/headless/src/controllers/commerce/search/headless-search.test.ts new file mode 100644 index 00000000000..e162284d80a --- /dev/null +++ b/packages/headless/src/controllers/commerce/search/headless-search.test.ts @@ -0,0 +1,39 @@ +import {Action} from '@reduxjs/toolkit'; +import {configuration} from '../../../app/common-reducers'; +import {contextReducer as commerceContext} from '../../../features/commerce/context/context-slice'; +import {queryReducer as commerceQuery} from '../../../features/commerce/query/query-slice'; +import {executeSearch} from '../../../features/commerce/search/search-actions'; +import {commerceSearchReducer as commerceSearch} from '../../../features/commerce/search/search-slice'; +import {buildMockCommerceEngine, MockCommerceEngine} from '../../../test'; +import {buildSearch, Search} from './headless-search'; + +describe('headless search', () => { + let search: Search; + let engine: MockCommerceEngine; + + beforeEach(() => { + engine = buildMockCommerceEngine(); + search = buildSearch(engine); + }); + + const expectContainAction = (action: Action) => { + const found = engine.actions.find((a) => a.type === action.type); + expect(engine.actions).toContainEqual(found); + }; + + it('adds the correct reducers to engine', () => { + expect(engine.addReducers).toHaveBeenCalledWith({ + commerceContext, + configuration, + commerceSearch, + commerceQuery, + }); + }); + + // eslint-disable-next-line @cspell/spellchecker + // TODO CAPI-244: Handle analytics + it('executeFirstSearch dispatches #executeSearch', () => { + search.executeFirstSearch(); + expectContainAction(executeSearch.pending); + }); +}); diff --git a/packages/headless/src/controllers/commerce/search/headless-search.ts b/packages/headless/src/controllers/commerce/search/headless-search.ts new file mode 100644 index 00000000000..9f6b2f7c0c5 --- /dev/null +++ b/packages/headless/src/controllers/commerce/search/headless-search.ts @@ -0,0 +1,75 @@ +import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; +import {ProductRecommendation} from '../../../api/search/search/product-recommendation'; +import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine'; +import {configuration} from '../../../app/common-reducers'; +import {SearchAction} from '../../../features/analytics/analytics-utils'; +import {contextReducer as commerceContext} from '../../../features/commerce/context/context-slice'; +import {queryReducer as commerceQuery} from '../../../features/commerce/query/query-slice'; +import {executeSearch} from '../../../features/commerce/search/search-actions'; +import {commerceSearchReducer as commerceSearch} from '../../../features/commerce/search/search-slice'; +import {loadReducerError} from '../../../utils/errors'; +import { + buildController, + Controller, +} from '../../controller/headless-controller'; + +export interface Search extends Controller { + /** + * Executes the first search. + */ + executeFirstSearch(analyticsEvent?: SearchAction): void; + + /** + * A scoped and simplified part of the headless state that is relevant to the `Search` controller. + */ + state: SearchState; +} + +export interface SearchState { + products: ProductRecommendation[]; + error: CommerceAPIErrorStatusResponse | null; + isLoading: boolean; + responseId: string; +} + +export function buildSearch(engine: CommerceEngine): Search { + if (!loadBaseSearchReducers(engine)) { + throw loadReducerError; + } + + const controller = buildController(engine); + const {dispatch} = engine; + const getState = () => engine.state; + + return { + ...controller, + + get state() { + return getState().commerceSearch; + }, + + // eslint-disable-next-line @cspell/spellchecker + // TODO CAPI-244: Handle analytics + executeFirstSearch() { + const firstSearchExecuted = engine.state.commerceSearch.responseId !== ''; + + if (firstSearchExecuted) { + return; + } + + dispatch(executeSearch()); + }, + }; +} + +function loadBaseSearchReducers( + engine: CommerceEngine +): engine is CommerceEngine { + engine.addReducers({ + commerceContext, + configuration, + commerceSearch, + commerceQuery, + }); + return true; +} diff --git a/packages/headless/src/features/commerce/common/actions.ts b/packages/headless/src/features/commerce/common/actions.ts new file mode 100644 index 00000000000..643ef4c4556 --- /dev/null +++ b/packages/headless/src/features/commerce/common/actions.ts @@ -0,0 +1,95 @@ +import {SortParam} from '../../../api/commerce/commerce-api-params'; +import {CommerceAPIRequest} from '../../../api/commerce/common/request'; +import {CommerceSuccessResponse} from '../../../api/commerce/common/response'; +import { + CartSection, + CategoryFacetSection, + CommerceContextSection, + CommercePaginationSection, + CommerceSortSection, + ConfigurationSection, + DateFacetSection, + FacetOrderSection, + FacetSection, + NumericFacetSection, + ProductListingV2Section, + VersionSection, +} from '../../../state/state-sections'; +import {PreparableAnalyticsAction} from '../../analytics/analytics-utils'; +import {StateNeededByFetchProductListingV2} from '../product-listing/product-listing-actions'; +import {SortBy, SortCriterion} from '../sort/sort'; + +export type StateNeededByQueryCommerceAPI = ConfigurationSection & + ProductListingV2Section & + CommerceContextSection & + CartSection & + Partial< + CommercePaginationSection & + CommerceSortSection & + FacetSection & + NumericFacetSection & + CategoryFacetSection & + DateFacetSection & + FacetOrderSection & + VersionSection + >; + +export interface QueryCommerceAPIThunkReturn { + /** The successful search response. */ + response: CommerceSuccessResponse; + analyticsAction: PreparableAnalyticsAction; +} + +export const buildCommerceAPIRequest = ( + state: StateNeededByQueryCommerceAPI +): CommerceAPIRequest => { + const facets = getFacets(state); + + const {view, user, ...restOfContext} = state.commerceContext; + return { + accessToken: state.configuration.accessToken, + url: state.configuration.platformUrl, + organizationId: state.configuration.organizationId, + ...restOfContext, + context: { + user, + view, + cart: state.cart.cartItems.map((id) => state.cart.cart[id]), + }, + facets, + ...(state.commercePagination && {page: state.commercePagination.page}), + ...(state.commerceSort && { + sort: getSort(state.commerceSort.appliedSort), + }), + }; +}; + +function getFacets(state: StateNeededByFetchProductListingV2) { + if (!state.facetOrder || !state.commerceFacetSet) { + return []; + } + + return state.facetOrder + .map((facetId) => state.commerceFacetSet![facetId].request) + .filter((facet) => facet.values.length > 0); +} + +function getSort(appliedSort: SortCriterion): SortParam['sort'] | undefined { + if (!appliedSort) { + return; + } + + if (appliedSort.by === SortBy.Relevance) { + return { + sortCriteria: SortBy.Relevance, + }; + } else { + return { + sortCriteria: SortBy.Fields, + fields: appliedSort.fields.map(({name, direction}) => ({ + field: name, + direction, + })), + }; + } +} diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-actions-loader.ts b/packages/headless/src/features/commerce/product-listing/product-listing-actions-loader.ts index 61ed1c8b133..4dac19858b8 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-actions-loader.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-actions-loader.ts @@ -15,10 +15,10 @@ import { LogFacetUpdateSortActionCreatorPayload, } from '../../facets/facet-set/facet-set-product-listing-v2-analytics-actions'; import { - FetchProductListingV2ThunkReturn, - fetchProductListing, - StateNeededByFetchProductListingV2, -} from './product-listing-actions'; + QueryCommerceAPIThunkReturn, + StateNeededByQueryCommerceAPI, +} from '../common/actions'; +import {fetchProductListing} from './product-listing-actions'; /** * The product listing action creators. @@ -32,9 +32,9 @@ export interface ProductListingActionCreators { * @returns A dispatchable action. */ fetchProductListing(): AsyncThunkAction< - FetchProductListingV2ThunkReturn, + QueryCommerceAPIThunkReturn, void, - AsyncThunkCommerceOptions + AsyncThunkCommerceOptions >; } diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts b/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts index 057e4749be5..aae1528d10d 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-actions.ts @@ -1,8 +1,5 @@ import {createAsyncThunk} from '@reduxjs/toolkit'; import {AsyncThunkCommerceOptions} from '../../../api/commerce/commerce-api-client'; -import {SelectedSortParam} from '../../../api/commerce/commerce-api-params'; -import {ProductListingV2Request} from '../../../api/commerce/product-listings/v2/product-listing-v2-request'; -import {ProductListingV2SuccessResponse} from '../../../api/commerce/product-listings/v2/product-listing-v2-response'; import {isErrorResponse} from '../../../api/search/search-api-client'; import { CartSection, @@ -15,9 +12,12 @@ import { ProductListingV2Section, VersionSection, } from '../../../state/state-sections'; -import {PreparableAnalyticsAction} from '../../analytics/analytics-utils'; import {logQueryError} from '../../search/search-analytics-actions'; -import {SortBy, SortCriterion} from '../sort/sort'; +import { + buildCommerceAPIRequest, + QueryCommerceAPIThunkReturn, + StateNeededByQueryCommerceAPI, +} from '../common/actions'; import {logProductListingV2Load} from './product-listing-analytics'; export type StateNeededByFetchProductListingV2 = ConfigurationSection & @@ -32,23 +32,17 @@ export type StateNeededByFetchProductListingV2 = ConfigurationSection & VersionSection >; -export interface FetchProductListingV2ThunkReturn { - /** The successful search response. */ - response: ProductListingV2SuccessResponse; - analyticsAction: PreparableAnalyticsAction; -} - export const fetchProductListing = createAsyncThunk< - FetchProductListingV2ThunkReturn, + QueryCommerceAPIThunkReturn, void, - AsyncThunkCommerceOptions + AsyncThunkCommerceOptions >( 'commerce/productListing/fetch', async (_action, {getState, dispatch, rejectWithValue, extra}) => { const state = getState(); const {apiClient} = extra; const fetched = await apiClient.getProductListing( - buildProductListingRequestV2(state) + buildCommerceAPIRequest(state) ); if (isErrorResponse(fetched)) { @@ -62,59 +56,3 @@ export const fetchProductListing = createAsyncThunk< }; } ); - -export const buildProductListingRequestV2 = ( - state: StateNeededByFetchProductListingV2 -): ProductListingV2Request => { - const facets = getFacets(state); - - const {view, user, ...restOfContext} = state.commerceContext; - return { - accessToken: state.configuration.accessToken, - url: state.configuration.platformUrl, - organizationId: state.configuration.organizationId, - ...restOfContext, - context: { - user, - view, - cart: state.cart.cartItems.map((id) => state.cart.cart[id]), - }, - facets, - ...(state.commercePagination && {page: state.commercePagination.page}), - ...(state.commerceSort && { - sort: getSort(state.commerceSort.appliedSort), - }), - }; -}; - -function getFacets(state: StateNeededByFetchProductListingV2) { - if (!state.facetOrder || !state.commerceFacetSet) { - return []; - } - - return state.facetOrder - .map((facetId) => state.commerceFacetSet![facetId].request) - .filter((facet) => facet.values.length > 0); -} - -function getSort( - appliedSort: SortCriterion -): SelectedSortParam['sort'] | undefined { - if (!appliedSort) { - return; - } - - if (appliedSort.by === SortBy.Relevance) { - return { - sortCriteria: SortBy.Relevance, - }; - } else { - return { - sortCriteria: SortBy.Fields, - fields: appliedSort.fields.map(({name, direction}) => ({ - field: name, - direction, - })), - }; - } -} diff --git a/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts b/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts index 1b3cbbbbd03..c25c22d81f5 100644 --- a/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts +++ b/packages/headless/src/features/commerce/product-listing/product-listing-slice.test.ts @@ -1,6 +1,6 @@ +import {buildMockCommerceFacetResponse} from '../../../test/mock-commerce-facet-response'; import {buildFetchProductListingV2Response} from '../../../test/mock-product-listing-v2'; import {buildMockProductRecommendation} from '../../../test/mock-product-recommendation'; -import {SortBy} from '../sort/sort'; import {fetchProductListing} from './product-listing-actions'; import {productListingV2Reducer} from './product-listing-slice'; import { @@ -21,20 +21,20 @@ describe('product-listing-v2-slice', () => { it('when a fetchProductListing fulfilled is received, should set the state to the received payload', () => { const result = buildMockProductRecommendation(); - const sortByRelevance = {sortCriteria: SortBy.Relevance}; - const sort = { - appliedSort: sortByRelevance, - availableSorts: [sortByRelevance], - }; + const facet = buildMockCommerceFacetResponse(); + const responseId = 'some-response-id'; const response = buildFetchProductListingV2Response({ products: [result], - sort, + facets: [facet], + responseId, }); const action = fetchProductListing.fulfilled(response, ''); const finalState = productListingV2Reducer(state, action); expect(finalState.products[0]).toEqual(result); + expect(finalState.facets[0]).toEqual(facet); + expect(finalState.responseId).toEqual(responseId); expect(finalState.isLoading).toBe(false); }); diff --git a/packages/headless/src/features/commerce/query/query-actions.ts b/packages/headless/src/features/commerce/query/query-actions.ts new file mode 100644 index 00000000000..fa7b0a93ae8 --- /dev/null +++ b/packages/headless/src/features/commerce/query/query-actions.ts @@ -0,0 +1,18 @@ +import {StringValue} from '@coveo/bueno'; +import {createAction} from '../../../ssr.index'; +import {validatePayload} from '../../../utils/validate-payload'; + +export interface UpdateQueryActionCreatorPayload { + /** + * The basic query expression (e.g., `acme tornado seeds`). + */ + query?: string; +} + +export const updateQuery = createAction( + 'query/updateQuery', + (payload: UpdateQueryActionCreatorPayload) => + validatePayload(payload, { + query: new StringValue(), + }) +); diff --git a/packages/headless/src/features/commerce/query/query-slice.test.ts b/packages/headless/src/features/commerce/query/query-slice.test.ts new file mode 100644 index 00000000000..1c369a16933 --- /dev/null +++ b/packages/headless/src/features/commerce/query/query-slice.test.ts @@ -0,0 +1,37 @@ +import {updateQuery} from './query-actions'; +import {queryReducer} from './query-slice'; +import {CommerceQueryState, getCommerceQueryInitialState} from './query-state'; + +describe('query slice', () => { + let state: CommerceQueryState; + + beforeEach(() => { + state = getCommerceQueryInitialState(); + }); + + it('should have initial state', () => { + expect(queryReducer(undefined, {type: ''})).toEqual({ + query: '', + }); + }); + + describe('updateQuery', () => { + const expectedState: CommerceQueryState = { + query: 'some query', + }; + + it('should handle updateQuery on initial state', () => { + expect(queryReducer(state, updateQuery({query: 'some query'}))).toEqual( + expectedState + ); + }); + + it('should handle updateQuery on existing state', () => { + state.query = 'another query'; + + expect(queryReducer(state, updateQuery({query: 'some query'}))).toEqual( + expectedState + ); + }); + }); +}); diff --git a/packages/headless/src/features/commerce/query/query-slice.ts b/packages/headless/src/features/commerce/query/query-slice.ts new file mode 100644 index 00000000000..4323fa6d375 --- /dev/null +++ b/packages/headless/src/features/commerce/query/query-slice.ts @@ -0,0 +1,14 @@ +import {createReducer} from '../../../ssr.index'; +import {updateQuery} from './query-actions'; +import {getCommerceQueryInitialState} from './query-state'; + +export const queryReducer = createReducer( + getCommerceQueryInitialState(), + + (builder) => { + builder.addCase(updateQuery, (state, action) => ({ + ...state, + ...action.payload, + })); + } +); diff --git a/packages/headless/src/features/commerce/query/query-state.ts b/packages/headless/src/features/commerce/query/query-state.ts new file mode 100644 index 00000000000..106b548e636 --- /dev/null +++ b/packages/headless/src/features/commerce/query/query-state.ts @@ -0,0 +1,7 @@ +export interface CommerceQueryState { + query?: string; +} + +export const getCommerceQueryInitialState: () => CommerceQueryState = () => ({ + query: '', +}); diff --git a/packages/headless/src/features/commerce/search/search-actions.ts b/packages/headless/src/features/commerce/search/search-actions.ts new file mode 100644 index 00000000000..adbff59dd7c --- /dev/null +++ b/packages/headless/src/features/commerce/search/search-actions.ts @@ -0,0 +1,42 @@ +import {createAsyncThunk} from '@reduxjs/toolkit'; +import {AsyncThunkCommerceOptions} from '../../../api/commerce/commerce-api-client'; +import {isErrorResponse} from '../../../api/search/search-api-client'; +import {CommerceQuerySection} from '../../../state/state-sections'; +import {logQueryError} from '../../search/search-analytics-actions'; +import { + buildCommerceAPIRequest, + QueryCommerceAPIThunkReturn, + StateNeededByQueryCommerceAPI, +} from '../common/actions'; +import {logProductListingV2Load} from '../product-listing/product-listing-analytics'; + +export type StateNeededByExecuteSearch = StateNeededByQueryCommerceAPI & + CommerceQuerySection; + +export const executeSearch = createAsyncThunk< + QueryCommerceAPIThunkReturn, + void, + AsyncThunkCommerceOptions +>( + 'commerce/search/executeSearch', + async (_action, {getState, dispatch, rejectWithValue, extra}) => { + const state = getState(); + const {apiClient} = extra; + const fetched = await apiClient.search({ + ...buildCommerceAPIRequest(state), + query: state.commerceQuery?.query, + }); + + if (isErrorResponse(fetched)) { + dispatch(logQueryError(fetched.error)); + return rejectWithValue(fetched.error); + } + + return { + response: fetched.success, + // eslint-disable-next-line @cspell/spellchecker + // TODO CAPI-244: Use actual search analytics action + analyticsAction: logProductListingV2Load(), + }; + } +); diff --git a/packages/headless/src/features/commerce/search/search-slice.test.ts b/packages/headless/src/features/commerce/search/search-slice.test.ts new file mode 100644 index 00000000000..855272f9c59 --- /dev/null +++ b/packages/headless/src/features/commerce/search/search-slice.test.ts @@ -0,0 +1,91 @@ +import {buildMockCommerceFacetResponse} from '../../../test/mock-commerce-facet-response'; +import {buildSearchResponse} from '../../../test/mock-commerce-search'; +import {buildMockProductRecommendation} from '../../../test/mock-product-recommendation'; +import {executeSearch} from './search-actions'; +import {commerceSearchReducer} from './search-slice'; +import { + CommerceSearchState, + getCommerceSearchInitialState, +} from './search-state'; + +describe('search-slice', () => { + let state: CommerceSearchState; + + beforeEach(() => { + state = getCommerceSearchInitialState(); + }); + + it('should have an initial state', () => { + expect(commerceSearchReducer(undefined, {type: ''})).toEqual( + getCommerceSearchInitialState() + ); + }); + + describe('when executeSearch.fulfilled', () => { + it('it updates the state with the received payload', () => { + const products = [buildMockProductRecommendation()]; + const facets = [buildMockCommerceFacetResponse()]; + const responseId = 'some-response-id'; + const response = buildSearchResponse({ + products, + facets, + responseId, + }); + + const action = executeSearch.fulfilled(response, ''); + const finalState = commerceSearchReducer(state, action); + + expect(finalState.products).toEqual(products); + expect(finalState.facets).toEqual(facets); + expect(finalState.responseId).toEqual(responseId); + expect(finalState.isLoading).toBe(false); + }); + + it('set the error to null on success', () => { + state.error = {message: 'message', statusCode: 500, type: 'type'}; + + const response = buildSearchResponse(); + + const action = executeSearch.fulfilled(response, ''); + const finalState = commerceSearchReducer(state, action); + expect(finalState.error).toBeNull(); + }); + }); + + describe('when executeSearch.rejected', () => { + const err = { + message: 'message', + statusCode: 500, + type: 'type', + }; + + it('sets the error', () => { + const action = {type: executeSearch.rejected.type, payload: err}; + const finalState = commerceSearchReducer(state, action); + + expect(finalState.error).toEqual(err); + expect(finalState.isLoading).toBe(false); + }); + + it('sets isLoading to false', () => { + state.isLoading = true; + + const action = {type: executeSearch.rejected.type, payload: err}; + const finalState = commerceSearchReducer(state, action); + + expect(finalState.error).toEqual(err); + expect(finalState.isLoading).toBe(false); + }); + }); + + describe('when executeSearch.pending', () => { + it('sets isLoading to true', () => { + state.isLoading = false; + + const pendingAction = executeSearch.pending(''); + const finalState = commerceSearchReducer(state, pendingAction); + + expect(finalState.isLoading).toBe(true); + }); + }); +}); diff --git a/packages/headless/src/features/commerce/search/search-slice.ts b/packages/headless/src/features/commerce/search/search-slice.ts new file mode 100644 index 00000000000..fe094dd0ca7 --- /dev/null +++ b/packages/headless/src/features/commerce/search/search-slice.ts @@ -0,0 +1,25 @@ +import {createReducer} from '../../../ssr.index'; +import {executeSearch} from './search-actions'; +import {getCommerceSearchInitialState} from './search-state'; + +export const commerceSearchReducer = createReducer( + getCommerceSearchInitialState(), + + (builder) => { + builder + .addCase(executeSearch.rejected, (state, action) => { + state.error = action.payload ? action.payload : null; + state.isLoading = false; + }) + .addCase(executeSearch.fulfilled, (state, action) => { + state.error = null; + state.facets = action.payload.response.facets; + state.products = action.payload.response.products; + state.responseId = action.payload.response.responseId; + state.isLoading = false; + }) + .addCase(executeSearch.pending, (state) => { + state.isLoading = true; + }); + } +); diff --git a/packages/headless/src/features/commerce/search/search-state.ts b/packages/headless/src/features/commerce/search/search-state.ts new file mode 100644 index 00000000000..9f41aca17a1 --- /dev/null +++ b/packages/headless/src/features/commerce/search/search-state.ts @@ -0,0 +1,19 @@ +import {CommerceAPIErrorStatusResponse} from '../../../api/commerce/commerce-api-error-response'; +import {ProductRecommendation} from '../../../api/search/search/product-recommendation'; +import {AnyFacetResponse} from '../facets/facet-set/interfaces/response'; + +export interface CommerceSearchState { + error: CommerceAPIErrorStatusResponse | null; + isLoading: boolean; + responseId: string; + products: ProductRecommendation[]; + facets: AnyFacetResponse[]; +} + +export const getCommerceSearchInitialState = (): CommerceSearchState => ({ + error: null, + isLoading: false, + responseId: '', + products: [], + facets: [], +}); diff --git a/packages/headless/src/features/commerce/sort/sort-slice.ts b/packages/headless/src/features/commerce/sort/sort-slice.ts index 1fe0aded1e4..b03e2b099db 100644 --- a/packages/headless/src/features/commerce/sort/sort-slice.ts +++ b/packages/headless/src/features/commerce/sort/sort-slice.ts @@ -1,5 +1,5 @@ import {createReducer} from '@reduxjs/toolkit'; -import {SortOption} from '../../../api/commerce/product-listings/v2/sort'; +import {SortOption} from '../../../api/commerce/common/sort'; import { buildRelevanceSortCriterion, SortBy, diff --git a/packages/headless/src/integration-tests/commerce.test.ts b/packages/headless/src/integration-tests/commerce.test.ts index 1444c859459..f42caabe057 100644 --- a/packages/headless/src/integration-tests/commerce.test.ts +++ b/packages/headless/src/integration-tests/commerce.test.ts @@ -4,11 +4,13 @@ import { buildCommerceEngine, buildProductListing, buildRelevanceSortCriterion, + buildSearch, buildSort, CommerceEngine, ProductListing, buildProductListingFacetGenerator, } from '../commerce.index'; +import {updateQuery} from '../features/commerce/query/query-actions'; import {getOrganizationEndpoints} from '../insight.index'; import {waitForNextStateChange} from '../test/functional-test-utils'; @@ -121,4 +123,17 @@ describe.skip('commerce', () => { // Have it reflected on the local state expect(facetController.state.values[0].state).toEqual('selected'); }); + + it('searches', async () => { + engine.dispatch(updateQuery({query: 'yellow'})); + const search = buildSearch(engine); + await waitForNextStateChange(engine, { + action: () => { + search.executeFirstSearch(); + }, + expectedSubscriberCalls: 4, + }); + + expect(search.state.products).not.toEqual([]); + }); }); diff --git a/packages/headless/src/state/commerce-app-state.ts b/packages/headless/src/state/commerce-app-state.ts index 729bd6f1f74..4e8a280add5 100644 --- a/packages/headless/src/state/commerce-app-state.ts +++ b/packages/headless/src/state/commerce-app-state.ts @@ -6,12 +6,16 @@ import { ProductListingV2Section, VersionSection, CommerceSortSection, + CommerceSearchSection, + CommerceQuerySection, CommerceFacetSetSection, FacetOrderSection, } from './state-sections'; export type CommerceAppState = ConfigurationSection & ProductListingV2Section & + CommerceSearchSection & + CommerceQuerySection & FacetOrderSection & CommerceFacetSetSection & CommercePaginationSection & diff --git a/packages/headless/src/state/state-sections.ts b/packages/headless/src/state/state-sections.ts index d1e4aac1360..0ee993ede76 100644 --- a/packages/headless/src/state/state-sections.ts +++ b/packages/headless/src/state/state-sections.ts @@ -10,6 +10,8 @@ import {CommerceContextState} from '../features/commerce/context/context-state'; import {CommerceFacetSetState} from '../features/commerce/facets/facet-set/facet-set-state'; import {CommercePaginationState} from '../features/commerce/pagination/pagination-state'; import {ProductListingV2State} from '../features/commerce/product-listing/product-listing-state'; +import {CommerceQueryState} from '../features/commerce/query/query-state'; +import {CommerceSearchState} from '../features/commerce/search/search-state'; import {CommerceSortState} from '../features/commerce/sort/sort-state'; import {ConfigurationState} from '../features/configuration/configuration-state'; import {ContextState} from '../features/context/context-state'; @@ -334,6 +336,20 @@ export interface ProductListingV2Section { productListing: ProductListingV2State; } +export interface CommerceSearchSection { + /** + * The information related to the commerce search endpoint. + */ + commerceSearch: CommerceSearchState; +} + +export interface CommerceQuerySection { + /** + * The current user query. + */ + commerceQuery: CommerceQueryState; +} + export interface StructuredSortSection { /** * The information related to sort when using a structured sort format. diff --git a/packages/headless/src/test/mock-commerce-search.ts b/packages/headless/src/test/mock-commerce-search.ts new file mode 100644 index 00000000000..659e7ba084c --- /dev/null +++ b/packages/headless/src/test/mock-commerce-search.ts @@ -0,0 +1,3 @@ +import {buildFetchProductListingV2Response} from './mock-product-listing-v2'; + +export {buildFetchProductListingV2Response as buildSearchResponse}; diff --git a/packages/headless/src/test/mock-commerce-state.ts b/packages/headless/src/test/mock-commerce-state.ts index 8a3511cf588..65cb7026e0f 100644 --- a/packages/headless/src/test/mock-commerce-state.ts +++ b/packages/headless/src/test/mock-commerce-state.ts @@ -3,6 +3,8 @@ import {getContextInitialState} from '../features/commerce/context/context-state import {getCommerceFacetSetInitialState} from '../features/commerce/facets/facet-set/facet-set-state'; import {getCommercePaginationInitialState} from '../features/commerce/pagination/pagination-state'; import {getProductListingV2InitialState} from '../features/commerce/product-listing/product-listing-state'; +import {getCommerceQueryInitialState} from '../features/commerce/query/query-state'; +import {getCommerceSearchInitialState} from '../features/commerce/search/search-state'; import {getCommerceSortInitialState} from '../features/commerce/sort/sort-state'; import {getConfigurationInitialState} from '../features/configuration/configuration-state'; import {getFacetOrderInitialState} from '../features/facets/facet-order/facet-order-state'; @@ -14,6 +16,8 @@ export function buildMockCommerceState( return { configuration: getConfigurationInitialState(), productListing: getProductListingV2InitialState(), + commerceSearch: getCommerceSearchInitialState(), + commerceQuery: getCommerceQueryInitialState(), facetOrder: getFacetOrderInitialState(), commerceFacetSet: getCommerceFacetSetInitialState(), commercePagination: getCommercePaginationInitialState(), diff --git a/packages/headless/src/test/mock-product-listing-v2.ts b/packages/headless/src/test/mock-product-listing-v2.ts index 82d40026594..5aff09f4302 100644 --- a/packages/headless/src/test/mock-product-listing-v2.ts +++ b/packages/headless/src/test/mock-product-listing-v2.ts @@ -1,11 +1,11 @@ -import {ProductListingV2SuccessResponse} from '../api/commerce/product-listings/v2/product-listing-v2-response'; -import {FetchProductListingV2ThunkReturn} from '../features/commerce/product-listing/product-listing-actions'; +import {CommerceSuccessResponse} from '../api/commerce/common/response'; +import {QueryCommerceAPIThunkReturn} from '../features/commerce/common/actions'; import {logProductListingV2Load} from '../features/commerce/product-listing/product-listing-analytics'; import {SortBy} from '../features/sort/sort'; export function buildFetchProductListingV2Response( - response: Partial = {} -): FetchProductListingV2ThunkReturn { + response: Partial = {} +): QueryCommerceAPIThunkReturn { return { response: { sort: response.sort ?? {