Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c40870a
wip
Tooni Nov 25, 2025
57428c9
basic typesafe version
Tooni Nov 25, 2025
0b1036b
refactor to dedupe a bit
Tooni Nov 25, 2025
c616a44
refactor types a bit
Tooni Nov 25, 2025
d8a47d5
see if this compiles
Tooni Nov 25, 2025
c9ae7e0
fix types
Tooni Nov 25, 2025
dc5d00a
is this working?
Tooni Nov 25, 2025
2404f1d
reduce diff
Tooni Nov 25, 2025
a75aa10
refactor a default value
Tooni Nov 25, 2025
c4d8341
dedupe an option
Tooni Nov 25, 2025
72011ea
update comment
Tooni Nov 25, 2025
8d9aced
fix a type
Tooni Nov 26, 2025
f8280c3
fix a type
Tooni Nov 26, 2025
215145a
checkpoint version with no build errors
Tooni Nov 26, 2025
7c4fdb3
fix slice tests
Tooni Nov 26, 2025
4be83f8
fix product listing slice
Tooni Nov 26, 2025
5149845
fix headless-product-listing.test
Tooni Nov 26, 2025
f163219
product listing slice new tests
Tooni Nov 26, 2025
ccc14c5
Merge remote-tracking branch 'origin/main' into COMHUB2-1228
Tooni Nov 26, 2025
fb43ff0
fix product listing selector tests
Tooni Nov 26, 2025
a8d5bab
minor refactor
Tooni Nov 26, 2025
86afe24
respond to ci
Tooni Nov 26, 2025
9738dc2
remove unused field
Tooni Nov 26, 2025
2913fd9
respond to a comment
Tooni Nov 26, 2025
531092a
fix export
Tooni Nov 26, 2025
d62cdaf
Merge branch 'main' into COMHUB2-1228
Tooni Nov 26, 2025
4053b0c
put position on spotlight
Tooni Nov 26, 2025
750212b
put responseId on result too
Tooni Nov 26, 2025
a36065d
fix test
Tooni Nov 26, 2025
290c634
Merge remote-tracking branch 'origin/main' into COMHUB2-1228
Tooni Nov 26, 2025
3c28bd6
rearrange tests a bit
Tooni Nov 26, 2025
aa8d27e
fix type error
Tooni Nov 26, 2025
03a2f6b
Merge remote-tracking branch 'origin/main' into COMHUB2-1228
Tooni Nov 27, 2025
4fc3b42
make minor improvement to typing
Tooni Nov 27, 2025
5c25e56
Merge branch 'main' into COMHUB2-1228
Tooni Nov 27, 2025
c1963bf
Merge branch 'main' into COMHUB2-1228
Tooni Nov 27, 2025
8d9d483
Merge remote-tracking branch 'origin/main' into COMHUB2-1228
Tooni Nov 28, 2025
8fa97f6
respond to @alexprudhomme's feedback
Tooni Nov 28, 2025
0faa490
Merge branch 'COMHUB2-1228' of github.com:coveo/ui-kit into COMHUB2-1228
Tooni Nov 28, 2025
4bac225
rearrange some imports
Tooni Nov 28, 2025
238bc9c
Merge branch 'main' into COMHUB2-1228
Tooni Nov 28, 2025
864b202
Merge branch 'main' into COMHUB2-1228
Tooni Nov 28, 2025
acd03d3
Merge remote-tracking branch 'origin/main' into COMHUB2-1228
Tooni Dec 2, 2025
186ea12
refactor: merge preprocessing functions
Tooni Dec 2, 2025
c57d67c
refactor: deduplicate some code for processing products/results
Tooni Dec 2, 2025
72b9f07
avoid type cast
Tooni Dec 2, 2025
fc1ea6c
reorder
Tooni Dec 2, 2025
731d261
Merge branch 'COMHUB2-1228' of github.com:coveo/ui-kit into COMHUB2-1228
Tooni Dec 2, 2025
bb9b549
test: refactor tests to check enableResults param with mocks
Tooni Dec 2, 2025
d2d8020
test: rearrange tests a bit
Tooni Dec 2, 2025
66c46e0
restore code that ai randomly deleted
Tooni Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/headless-react/src/__tests__/mock-products.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {randomUUID} from 'node:crypto';
import type {Product} from '@coveo/headless/ssr-commerce';
import {ResultType} from '@coveo/headless/ssr-commerce';

const createMockProduct = (overrides: Partial<Product> = {}): Product => ({
additionalFields: {},
Expand All @@ -24,6 +25,7 @@ const createMockProduct = (overrides: Partial<Product> = {}): Product => ({
permanentid: randomUUID(),
position: 1,
totalNumberOfChildren: 0,
resultType: ResultType.PRODUCT,
...overrides,
});

Expand Down
16 changes: 13 additions & 3 deletions packages/headless/src/api/commerce/commerce-api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './commerce-api-client.js';
import type {FilterableCommerceAPIRequest} from './common/request.js';
import type {CommerceResponse} from './common/response.js';
import type {CommerceListingRequest} from './listing/request.js';
import type {CommerceRecommendationsRequest} from './recommendations/recommendations-request.js';

describe('commerce api client', () => {
Expand Down Expand Up @@ -81,7 +82,10 @@ describe('commerce api client', () => {
};

it('#getProductListing should call the platform endpoint with the correct arguments', async () => {
const request = await buildCommerceAPIRequest();
const request: CommerceListingRequest = {
...(await buildCommerceAPIRequest()),
enableResults: false,
};

mockPlatformCall({
ok: true,
Expand Down Expand Up @@ -279,7 +283,10 @@ describe('commerce api client', () => {
});

it('should return error response on failure', async () => {
const request = await buildCommerceAPIRequest();
const request: CommerceListingRequest = {
...(await buildCommerceAPIRequest()),
enableResults: false,
};

const expectedError = {
statusCode: 401,
Expand All @@ -300,7 +307,10 @@ describe('commerce api client', () => {
});

it('should return success response on success', async () => {
const request = await buildCommerceAPIRequest();
const request: CommerceListingRequest = {
...(await buildCommerceAPIRequest()),
enableResults: false,
};

const expectedBody: CommerceResponse = {
products: [],
Expand Down
18 changes: 11 additions & 7 deletions packages/headless/src/api/commerce/commerce-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ import type {
CommerceAPIErrorResponse,
CommerceAPIErrorStatusResponse,
} from './commerce-api-error-response.js';
import {
type FilterableCommerceAPIRequest,
getRequestOptions,
} from './common/request.js';
import {getRequestOptions} from './common/request.js';
import type {CommerceSuccessResponse} from './common/response.js';
import type {
CommerceFacetSearchRequest,
FacetSearchType,
} from './facet-search/facet-search-request.js';
import type {CommerceListingRequest} from './listing/request.js';
import type {ListingCommerceSuccessResponse} from './listing/response.js';
import {
buildRecommendationsRequest,
type CommerceRecommendationsRequest,
Expand Down Expand Up @@ -74,10 +73,15 @@ export class CommerceAPIClient implements CommerceFacetSearchAPIClient {
constructor(private options: CommerceAPIClientOptions) {}

async getProductListing(
req: FilterableCommerceAPIRequest
): Promise<CommerceAPIResponse<CommerceSuccessResponse>> {
req: CommerceListingRequest
): Promise<CommerceAPIResponse<ListingCommerceSuccessResponse>> {
const requestOptions = getRequestOptions(req, 'listing');
return this.query({
...getRequestOptions(req, 'listing'),
...requestOptions,
requestParams: {
...requestOptions.requestParams,
enableResults: req.enableResults,
},
...this.options,
});
}
Expand Down
4 changes: 4 additions & 0 deletions packages/headless/src/api/commerce/commerce-api-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface ContextParam {
context: ContextParams;
}

export interface EnableResultsParam {
enableResults: boolean;
}

type ProductParam = {
productId: string;
};
Expand Down
12 changes: 5 additions & 7 deletions packages/headless/src/api/commerce/common/product.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {HighlightKeyword} from '../../../utils/highlight.js';
import type {ResultPosition, ResultType} from './result.js';

export type ChildProduct = Omit<
BaseProduct,
Expand Down Expand Up @@ -143,13 +144,10 @@ export interface BaseProduct {
* The ID of the response that returned the product.
*/
responseId?: string;
}

export interface Product extends BaseProduct {
/**
* The 1-based product's position across the non-paginated result set.
*
* For example, if the product is the third one on the second page, and there are 10 products per page, its position is 13 (not 3).
* The result type of the product.
*/
position: number;
resultType: ResultType.PRODUCT | ResultType.CHILD_PRODUCT;
}

export interface Product extends ResultPosition, BaseProduct {}
54 changes: 54 additions & 0 deletions packages/headless/src/api/commerce/common/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {BaseProduct, Product} from './product.js';

export enum ResultType {
CHILD_PRODUCT = 'childProduct',
PRODUCT = 'product',
SPOTLIGHT = 'spotlight',
}

export interface BaseSpotlightContent {
/**
* The URI to navigate to when the spotlight content is clicked.
*/
clickUri: string;
/**
* The image URL for desktop display.
*/
desktopImage: string;
/**
* The image URL for mobile display.
*/
mobileImage?: string;
/**
* The name of the spotlight content.
*/
name?: string;
/**
* The description of the spotlight content.
*/
description?: string;
/**
* The ID of the response that returned the spotlight content.
*/
responseId?: string;
/**
* The result type identifier, always SPOTLIGHT for spotlight content.
*/
resultType: ResultType.SPOTLIGHT;
}

export interface ResultPosition {
/**
* The 1-based result's position across the non-paginated result set.
*
* For example, if the result is the third one on the second page, and there are 10 results per page, its position is 13 (not 3).
*/
position: number;
}

export interface SpotlightContent
extends ResultPosition,
BaseSpotlightContent {}

export type BaseResult = BaseProduct | BaseSpotlightContent;
export type Result = Product | SpotlightContent;
5 changes: 5 additions & 0 deletions packages/headless/src/api/commerce/listing/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {EnableResultsParam} from '../commerce-api-params.js';
import type {FilterableCommerceAPIRequest} from '../common/request.js';

export type CommerceListingRequest = FilterableCommerceAPIRequest &
EnableResultsParam;
7 changes: 7 additions & 0 deletions packages/headless/src/api/commerce/listing/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {CommerceSuccessResponse} from '../common/response.js';
import type {BaseResult} from '../common/result.js';

export interface ListingCommerceSuccessResponse
extends CommerceSuccessResponse {
results: BaseResult[];
}
1 change: 1 addition & 0 deletions packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export type {
export {buildInstantProducts} from './controllers/commerce/instant-products/headless-instant-products.js';
export type {
ProductListing,
ProductListingOptions,
ProductListingState,
} from './controllers/commerce/product-listing/headless-product-listing.js';
export {buildProductListing} from './controllers/commerce/product-listing/headless-product-listing.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@ import {
facetResponseSelector,
isFacetLoadingResponseSelector,
} from './facets/headless-product-listing-facet-options.js';
import {
buildProductListing,
type ProductListing,
} from './headless-product-listing.js';
import {buildProductListing} from './headless-product-listing.js';

describe('headless product-listing', () => {
let productListing: ProductListing;
let engine: MockedCommerceEngine;

beforeEach(() => {
engine = buildMockCommerceEngine(buildMockCommerceState());
productListing = buildProductListing(engine);
});

afterEach(() => {
Expand All @@ -57,8 +52,8 @@ describe('headless product-listing', () => {

expect(buildProductListingSubControllers).toHaveBeenCalledWith(engine, {
responseIdSelector,
fetchProductsActionCreator: ProductListingActions.fetchProductListing,
fetchMoreProductsActionCreator: ProductListingActions.fetchMoreProducts,
fetchProductsActionCreator: expect.any(Function),
fetchMoreProductsActionCreator: expect.any(Function),
facetResponseSelector,
isFacetLoadingResponseSelector,
requestIdSelector,
Expand All @@ -75,7 +70,58 @@ describe('headless product-listing', () => {
});
});

it('creates closures for fetching products that capture default enableResults=false', () => {
const buildProductListingSubControllers = vi.spyOn(
SubControllers,
'buildProductListingSubControllers'
);
const fetchProductListingMock = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);
const fetchMoreProductsMock = vi.spyOn(
ProductListingActions,
'fetchMoreProducts'
);

buildProductListing(engine);

const callArgs = buildProductListingSubControllers.mock.calls[0][1];
callArgs.fetchProductsActionCreator();
expect(fetchProductListingMock).toHaveBeenCalledWith({
enableResults: false,
});

callArgs.fetchMoreProductsActionCreator();
expect(fetchMoreProductsMock).toHaveBeenCalledWith({enableResults: false});
});

it('creates closures for fetching products that capture enableResults=true', () => {
const buildProductListingSubControllers = vi.spyOn(
SubControllers,
'buildProductListingSubControllers'
);
const fetchProductListingMock = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);
const fetchMoreProductsMock = vi.spyOn(
ProductListingActions,
'fetchMoreProducts'
);

buildProductListing(engine, {enableResults: true});

const callArgs = buildProductListingSubControllers.mock.calls[0][1];
callArgs.fetchProductsActionCreator();
expect(fetchProductListingMock).toHaveBeenCalledWith({enableResults: true});

callArgs.fetchMoreProductsActionCreator();
expect(fetchMoreProductsMock).toHaveBeenCalledWith({enableResults: true});
});

it('adds the correct reducers to engine', () => {
buildProductListing(engine);
expect(engine.addReducers).toHaveBeenCalledWith({
productListing: productListingReducer,
commerceContext: contextReducer,
Expand All @@ -90,32 +136,62 @@ describe('headless product-listing', () => {
);
const child = {permanentid: 'childPermanentId'} as ChildProduct;

const productListing = buildProductListing(engine);
productListing.promoteChildToParent(child);

expect(promoteChildToParent).toHaveBeenCalledWith({
child,
});
});

it('#refresh dispatches #fetchProductListing', () => {
it('#refresh dispatches #fetchProductListing with enableResults=false by default', () => {
const fetchProductListing = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);

const productListing = buildProductListing(engine);
productListing.refresh();

expect(fetchProductListing).toHaveBeenCalled();
expect(fetchProductListing).toHaveBeenCalledWith({enableResults: false});
});

it('#executeFirstRequest dispatches #fetchProductListing', () => {
const executeRequest = vi.spyOn(
it('#refresh dispatches #fetchProductListing with enableResults=true when specified', () => {
const fetchProductListing = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);
const productListingWithResults = buildProductListing(engine, {
enableResults: true,
});

productListingWithResults.refresh();

expect(fetchProductListing).toHaveBeenCalledWith({enableResults: true});
});

it('#executeFirstRequest dispatches #fetchProductListing with enableResults=false by default', () => {
const executeRequest = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);
const productListing = buildProductListing(engine);
productListing.executeFirstRequest();

expect(executeRequest).toHaveBeenCalled();
expect(executeRequest).toHaveBeenCalledWith({enableResults: false});
});

it('#executeFirstRequest dispatches #fetchProductListing with enableResults=true when specified', () => {
const executeRequest = vi.spyOn(
ProductListingActions,
'fetchProductListing'
);
const productListingWithResults = buildProductListing(engine, {
enableResults: true,
});

productListingWithResults.executeFirstRequest();

expect(executeRequest).toHaveBeenCalledWith({enableResults: true});
});
});
Loading
Loading