Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commerce): use sub-controllers in recs controller #3854

Merged
merged 7 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {AsyncThunkOptions} from '../../../app/async-thunk-options';

export type FetchResultsActionCreator = () => AsyncThunkAction<
unknown,
void,
unknown,
AsyncThunkOptions<unknown>
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
} from '../../../../test/mock-engine-v2';
import {
buildCorePagination,
CorePaginationOptions,
Pagination,
PaginationOptions,
} from './headless-core-commerce-pagination';

jest.mock('../../../../features/commerce/pagination/pagination-actions');
Expand All @@ -25,7 +25,7 @@ describe('core pagination', () => {
const fetchResultsActionCreator = jest.fn();
const slotId = 'recommendations-slot-id';

function initPagination(options: PaginationOptions = {}) {
function initPagination(options: CorePaginationOptions = {}) {
engine = buildMockCommerceEngine(buildMockCommerceState());

pagination = buildCorePagination(engine, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,24 @@ export interface PaginationState {
totalPages: number;
}

export interface PaginationOptions {
export interface CorePaginationOptions {
slotId?: string;
/**
* Recs slot id, or none for listings and search
* The number of products to fetch per page.
*/
slotId?: string;
pageSize?: number;
}

export interface CorePaginationProps {
fetchResultsActionCreator: FetchResultsActionCreator;
options?: PaginationOptions;
options?: CorePaginationOptions;
}

export type PaginationProps = Omit<
CorePaginationProps,
'fetchResultsActionCreator'
>;
export type PaginationOptions = Omit<CorePaginationOptions, 'slotId'>;

export interface PaginationProps {
options?: PaginationOptions;
}

const optionsSchema = new Schema({
pageSize: new NumberValue({min: 1, max: 1000, required: false}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,75 +7,130 @@ import * as CorePagination from '../pagination/headless-core-commerce-pagination
import * as CoreInteractiveResult from '../result-list/headless-core-interactive-result';
import * as CoreSort from '../sort/headless-core-commerce-sort';
import {
BaseSolutionTypeSubControllers,
buildBaseSolutionTypeControllers,
buildSolutionTypeSubControllers,
SolutionTypeSubControllers,
SearchAndListingSubControllers,
} from './headless-sub-controller';

describe('sub controllers', () => {
let engine: MockedCommerceEngine;
let subControllers: SolutionTypeSubControllers;
const mockResponseIdSelector = jest.fn();
const mockFetchResultsActionCreator = jest.fn();

function initSubControllers() {
engine = buildMockCommerceEngine(buildMockCommerceState());

subControllers = buildSolutionTypeSubControllers(engine, {
responseIdSelector: mockResponseIdSelector,
fetchResultsActionCreator: mockFetchResultsActionCreator,
});
}

beforeEach(() => {
initSubControllers();
engine = buildMockCommerceEngine(buildMockCommerceState());
});

afterEach(() => {
jest.clearAllMocks();
});

it('#interactiveResult builds interactive result controller', () => {
const buildCoreInteractiveResultMock = jest.spyOn(
CoreInteractiveResult,
'buildCoreInteractiveResult'
);

const props = {
options: {
product: {
productId: '1',
name: 'Product name',
price: 17.99,
it.each([
{
name: 'buildSolutionTypeSubControllers',
subControllersBuilder: buildSolutionTypeSubControllers,
},
{
name: 'buildBaseSolutionTypeControllers',
subControllersBuilder: buildBaseSolutionTypeControllers,
},
])(
'#interactiveResult builds interactive result controller',
({
subControllersBuilder,
}: {
subControllersBuilder:
| typeof buildSolutionTypeSubControllers
| typeof buildBaseSolutionTypeControllers;
}) => {
const subControllers = subControllersBuilder(engine, {
responseIdSelector: mockResponseIdSelector,
fetchResultsActionCreator: mockFetchResultsActionCreator,
});
const buildCoreInteractiveResultMock = jest.spyOn(
CoreInteractiveResult,
'buildCoreInteractiveResult'
);

const props = {
options: {
product: {
productId: '1',
name: 'Product name',
price: 17.99,
},
position: 1,
},
position: 1,
},
};
};

const interactiveResult = subControllers.interactiveResult({
...props,
});

expect(interactiveResult).toEqual(
buildCoreInteractiveResultMock.mock.results[0].value
);
}
);

const interactiveResult = subControllers.interactiveResult({
...props,
describe('#buildSolutionTypeSubControllers', () => {
let subControllers: SearchAndListingSubControllers;

beforeEach(() => {
subControllers = buildSolutionTypeSubControllers(engine, {
responseIdSelector: mockResponseIdSelector,
fetchResultsActionCreator: mockFetchResultsActionCreator,
});
});

expect(interactiveResult).toEqual(
buildCoreInteractiveResultMock.mock.results[0].value
);
});
it('#pagination builds pagination controller', () => {
const buildCorePaginationMock = jest.spyOn(
CorePagination,
'buildCorePagination'
);

it('#pagination builds pagination controller', () => {
const buildCorePaginationMock = jest.spyOn(
CorePagination,
'buildCorePagination'
);
const pagination = subControllers.pagination();

const pagination = subControllers.pagination();
expect(pagination).toEqual(buildCorePaginationMock.mock.results[0].value);
});

it('#sort builds sort controller', () => {
const buildCoreSortMock = jest.spyOn(CoreSort, 'buildCoreSort');

const sort = subControllers.sort();

expect(pagination).toEqual(buildCorePaginationMock.mock.results[0].value);
expect(sort).toEqual(buildCoreSortMock.mock.results[0].value);
});
});

it('#sort builds sort controller', () => {
const buildCoreSortMock = jest.spyOn(CoreSort, 'buildCoreSort');
describe('#buildRecommendationsSubControllers', () => {
const slotId = 'recommendations-slot-id';
let subControllers: BaseSolutionTypeSubControllers;

const sort = subControllers.sort();
beforeEach(() => {
subControllers = buildBaseSolutionTypeControllers(engine, {
slotId,
responseIdSelector: mockResponseIdSelector,
fetchResultsActionCreator: mockFetchResultsActionCreator,
});
});

expect(sort).toEqual(buildCoreSortMock.mock.results[0].value);
it('#pagination builds pagination controller with slot id', () => {
const buildCorePaginationMock = jest.spyOn(
CorePagination,
'buildCorePagination'
);

const pagination = subControllers.pagination();

expect(pagination).toEqual(buildCorePaginationMock.mock.results[0].value);
expect(buildCorePaginationMock).toHaveBeenCalledWith(engine, {
fetchResultsActionCreator: mockFetchResultsActionCreator,
options: {
slotId,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,43 @@ import {
SortProps,
} from '../sort/headless-core-commerce-sort';

export interface SolutionTypeSubControllers {
export interface BaseSolutionTypeSubControllers {
interactiveResult: (props: InteractiveResultProps) => InteractiveResult;
pagination: (props?: PaginationProps) => Pagination;
sort: (props?: SortProps) => Sort;
}

export type SearchAndListingSubControllers = BaseSolutionTypeSubControllers & {
sort: (props?: SortProps) => Sort;
};

interface SubControllerProps {
responseIdSelector: (state: CommerceEngineState) => string;
fetchResultsActionCreator: FetchResultsActionCreator;
slotId?: string;
}

export function buildSolutionTypeSubControllers(
engine: CommerceEngine,
subControllerProps: SubControllerProps
): SolutionTypeSubControllers {
const {responseIdSelector, fetchResultsActionCreator} = subControllerProps;
): SearchAndListingSubControllers {
const {fetchResultsActionCreator} = subControllerProps;
return {
...buildBaseSolutionTypeControllers(engine, subControllerProps),
sort(props?: SortProps) {
return buildCoreSort(engine, {
...props,
fetchResultsActionCreator,
});
},
};
}

export function buildBaseSolutionTypeControllers(
engine: CommerceEngine,
subControllerProps: SubControllerProps
): BaseSolutionTypeSubControllers {
const {responseIdSelector, fetchResultsActionCreator, slotId} =
subControllerProps;
return {
interactiveResult(props: InteractiveResultProps) {
return buildCoreInteractiveResult(engine, {
Expand All @@ -45,12 +66,10 @@ export function buildSolutionTypeSubControllers(
pagination(props?: PaginationProps) {
return buildCorePagination(engine, {
...props,
fetchResultsActionCreator,
});
},
sort(props?: SortProps) {
return buildCoreSort(engine, {
...props,
options: {
...props?.options,
slotId,
},
fetchResultsActionCreator,
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {
} from '../../controller/headless-controller';
import {
buildSolutionTypeSubControllers,
SolutionTypeSubControllers,
SearchAndListingSubControllers,
} from '../core/sub-controller/headless-sub-controller';

/**
* The `ProductListing` controller exposes a method for retrieving product listing content in a commerce interface.
*/
export interface ProductListing extends Controller, SolutionTypeSubControllers {
export interface ProductListing
extends Controller,
SearchAndListingSubControllers {
/**
* Fetches the product listing.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ import {
buildController,
Controller,
} from '../../controller/headless-controller';
import {
BaseSolutionTypeSubControllers,
buildBaseSolutionTypeControllers,
} from '../core/sub-controller/headless-sub-controller';

/**
* The `Recommendations` controller exposes a method for retrieving recommendations content in a commerce interface.
*/
export interface Recommendations extends Controller {
export interface Recommendations
extends Controller,
BaseSolutionTypeSubControllers {
/**
* Fetches the recommendations.
*/
Expand Down Expand Up @@ -84,9 +90,15 @@ export function buildRecommendations(
(state: CommerceEngineState) => state.recommendations[slotId]!,
(recommendations) => recommendations
);
const subControllers = buildBaseSolutionTypeControllers(engine, {
slotId,
responseIdSelector: (state) => state.recommendations[slotId]!.responseId,
fetchResultsActionCreator: () => fetchRecommendations({slotId}),
});

return {
...controller,
...subControllers,

get state() {
return recommendationStateSelector(engine.state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import {
} from '../../controller/headless-controller';
import {
buildSolutionTypeSubControllers,
SolutionTypeSubControllers,
SearchAndListingSubControllers,
} from '../core/sub-controller/headless-sub-controller';

export interface Search extends Controller, SolutionTypeSubControllers {
export interface Search extends Controller, SearchAndListingSubControllers {
/**
* Executes the first search.
*/
Expand Down
Loading