Skip to content

Commit

Permalink
feat(commerce): add commerce category facets (#3495)
Browse files Browse the repository at this point in the history
https://coveord.atlassian.net/browse/CAPI-90

Accompanying branch in barca:
coveo/barca-sports#191

---------

Co-authored-by: Nicholas-David Labarre <[email protected]>
Co-authored-by: Nicholas Labarre <[email protected]>
  • Loading branch information
3 people authored Mar 25, 2024
1 parent bcd1ffd commit 138a8b1
Show file tree
Hide file tree
Showing 29 changed files with 2,144 additions and 366 deletions.
4 changes: 2 additions & 2 deletions packages/headless/src/api/commerce/commerce-api-params.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {AnyCommerceFacetRequest} from '../../features/commerce/facets/facet-set/interfaces/request';
import {AnyFacetRequest} from '../../features/commerce/facets/facet-set/interfaces/request';
import {SortOption} from './common/sort';

export interface TrackingIdParam {
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface CartItemParam {
}

export interface FacetsParam {
facets?: AnyCommerceFacetRequest[];
facets?: AnyFacetRequest[];
}

export interface PageParam {
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export {
export {buildProductListingSort} from './controllers/commerce/product-listing/sort/headless-product-listing-sort';
export {buildSearchSort} from './controllers/commerce/search/sort/headless-search-sort';

export type {CategoryFacet} from './controllers/commerce/core/facets/category/headless-commerce-category-facet';
export type {RegularFacet} from './controllers/commerce/core/facets/regular/headless-commerce-regular-facet';
export type {NumericFacet} from './controllers/commerce/core/facets/numeric/headless-commerce-numeric-facet';
export type {DateFacet} from './controllers/commerce/core/facets/date/headless-commerce-date-facet';
Expand All @@ -108,6 +109,8 @@ export type {
NumericFacetValue,
DateRangeRequest,
DateFacetValue,
CategoryFacetValueRequest,
CategoryFacetValue,
} from './controllers/commerce/core/facets/headless-core-commerce-facet';
export type {ProductListingFacetGenerator} from './controllers/commerce/product-listing/facets/headless-product-listing-facet-generator';
export {buildProductListingFacetGenerator} from './controllers/commerce/product-listing/facets/headless-product-listing-facet-generator';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
import {
CategoryFacetValueRequest,
CommerceFacetRequest,
} from '../../../../../features/commerce/facets/facet-set/interfaces/request';
import {CategoryFacetValue} from '../../../../../features/commerce/facets/facet-set/interfaces/response';
import {
toggleSelectCategoryFacetValue,
updateCategoryFacetNumberOfValues,
} from '../../../../../features/facets/category-facet-set/category-facet-set-actions';
import {CommerceAppState} from '../../../../../state/commerce-app-state';
import {buildMockCommerceFacetRequest} from '../../../../../test/mock-commerce-facet-request';
import {buildMockCategoryFacetResponse} from '../../../../../test/mock-commerce-facet-response';
import {buildMockCommerceFacetSlice} from '../../../../../test/mock-commerce-facet-slice';
import {buildMockCategoryFacetValue} from '../../../../../test/mock-commerce-facet-value';
import {buildMockCommerceState} from '../../../../../test/mock-commerce-state';
import {
MockedCommerceEngine,
buildMockCommerceEngine,
} from '../../../../../test/mock-engine-v2';
import {commonOptions} from '../../../product-listing/facets/headless-product-listing-facet-options';
import {
CategoryFacet,
CategoryFacetOptions,
buildCategoryFacet,
} from './headless-commerce-category-facet';

jest.mock(
'../../../../../features/facets/category-facet-set/category-facet-set-actions'
);

describe('CategoryFacet', () => {
const facetId: string = 'category_facet_id';
let engine: MockedCommerceEngine;
let state: CommerceAppState;
let options: CategoryFacetOptions;
let facet: CategoryFacet;

function initEngine(preloadedState = buildMockCommerceState()) {
engine = buildMockCommerceEngine(preloadedState);
}

function initCategoryFacet() {
facet = buildCategoryFacet(engine, options);
}

function setFacetState(
config: Partial<CommerceFacetRequest<CategoryFacetValueRequest>> = {},
moreValuesAvailable = false
) {
state.commerceFacetSet[facetId] = buildMockCommerceFacetSlice({
request: buildMockCommerceFacetRequest({
facetId,
type: 'hierarchical',
...config,
}),
});
state.productListing.facets = [
buildMockCategoryFacetResponse({
moreValuesAvailable,
facetId,
type: 'hierarchical',
values: (config.values as CategoryFacetValue[]) ?? [],
}),
];
}

// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-90: Test facet search
/*function setFacetSearch() {
state.facetSearchSet[facetId] = buildMockFacetSearch();
}*/

beforeEach(() => {
jest.resetAllMocks();

options = {
facetId,
...commonOptions,
};

state = buildMockCommerceState();
setFacetState();
// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-90: Test facet search
// setFacetSearch();

initEngine(state);
initCategoryFacet();
});

describe('initialization', () => {
it('initializes', () => {
expect(facet).toBeTruthy();
});

it('exposes #subscribe method', () => {
expect(facet.subscribe).toBeTruthy();
});
});

it('#toggleSelect dispatches #toggleSelectCategoryFacetValue with correct payload', () => {
const facetValue = buildMockCategoryFacetValue();
facet.toggleSelect(facetValue);

expect(toggleSelectCategoryFacetValue).toHaveBeenCalledWith({
facetId,
selection: facetValue,
});
});

it('#showLessValues dispatches #updateCategoryFacetNumberOfValues with correct payload', () => {
facet.showLessValues();

expect(updateCategoryFacetNumberOfValues).toHaveBeenCalledWith({
facetId,
numberOfValues: 5,
});
});

it('#showMoreValues dispatches #updateCategoryFacetNumberOfValues with correct payload', () => {
facet.showMoreValues();

expect(updateCategoryFacetNumberOfValues).toHaveBeenCalledWith({
facetId,
numberOfValues: 5,
});
});

describe('#state', () => {
describe('#activeValue', () => {
it('when no value is selected, returns undefined', () => {
expect(facet.state.activeValue).toBeUndefined();
});
it('when a value is selected, returns the selected value', () => {
const activeValue = buildMockCategoryFacetValue({
state: 'selected',
});
setFacetState({
values: [activeValue, buildMockCategoryFacetValue()],
});

expect(facet.state.activeValue).toBe(activeValue);
});
});

describe('#canShowLessValues', () => {
describe('when no value is selected', () => {
it('when there are no values, returns false', () => {
expect(facet.state.canShowLessValues).toBe(false);
});
it('when there are fewer values than default number of values, returns false', () => {
setFacetState({
values: [buildMockCategoryFacetValue()],
});

expect(facet.state.canShowLessValues).toBe(false);
});
it('when there are more values than default number of values, returns true', () => {
setFacetState({
values: [
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
],
});

expect(facet.state.canShowLessValues).toBe(false);
});
});

describe('when a value is selected', () => {
it('when selected value has no children, returns false', () => {
setFacetState({
values: [
buildMockCategoryFacetValue({
state: 'selected',
}),
],
});

expect(facet.state.canShowLessValues).toBe(false);
});
it('when selected value fewer children than default number of values, returns false', () => {
setFacetState({
values: [
buildMockCategoryFacetValue({
state: 'selected',
children: [buildMockCategoryFacetValue()],
}),
],
});

expect(facet.state.canShowLessValues).toBe(false);
});
it('when selected value has more children than default number of values, return true', () => {
setFacetState({
values: [
buildMockCategoryFacetValue({
state: 'selected',
children: [
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
buildMockCategoryFacetValue(),
],
}),
],
});

expect(facet.state.canShowLessValues).toBe(true);
});
});
});

describe('#canShowMoreValues', () => {
describe('when no value is selected', () => {
it('when there are no more values available, returns false', () => {
expect(facet.state.canShowMoreValues).toBe(false);
});

it('when there are more values available, returns true', () => {
setFacetState({}, true);

expect(facet.state.canShowMoreValues).toBe(true);
});
});

describe('when a value is selected', () => {
it('when selected values has no more values available, returns false', () => {
setFacetState({
values: [buildMockCategoryFacetValue({state: 'selected'})],
});

expect(facet.state.canShowMoreValues).toBe(false);
});
it('when selected value has more values available, returns true', () => {
setFacetState({
values: [
buildMockCategoryFacetValue({
state: 'selected',
moreValuesAvailable: true,
}),
],
});

expect(facet.state.canShowMoreValues).toBe(true);
});
});
});

describe('#hasActiveValues', () => {
it('when no value is selected, returns false', () => {
expect(facet.state.hasActiveValues).toBe(false);
});

it('when a value is selected, returns true', () => {
setFacetState({
values: [buildMockCategoryFacetValue({state: 'selected'})],
});

expect(facet.state.hasActiveValues).toBe(true);
});
});

describe('#selectedValueAncestry', () => {
it('when no value is selected, returns empty array', () => {
expect(facet.state.selectedValueAncestry).toEqual([]);
});

it('when a value is selected, returns the selected value ancestry', () => {
const activeValue = buildMockCategoryFacetValue({
value: 'c',
path: ['a', 'b', 'c'],
state: 'selected',
children: [
buildMockCategoryFacetValue({
value: 'd',
path: ['a', 'b', 'c', 'd'],
}),
buildMockCategoryFacetValue({
value: 'e',
path: ['a', 'b', 'c', 'e'],
}),
],
});
const parentValue = buildMockCategoryFacetValue({
value: 'b',
path: ['a', 'b'],
children: [activeValue],
});

const rootValue = buildMockCategoryFacetValue({
value: 'a',
path: ['a'],
children: [parentValue],
});

setFacetState({
values: [rootValue],
});

expect(facet.state.selectedValueAncestry).toEqual([
rootValue,
parentValue,
activeValue,
]);
});
});
});
});
Loading

0 comments on commit 138a8b1

Please sign in to comment.