diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index a100710f020..4141b381521 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -78,7 +78,7 @@ export interface ControllerDefinitionWithProps< */ buildWithProps( engine: SSRCommerceEngine, - props: TProps, + props?: TProps, solutionType?: SolutionType ): TController & ControllerWithKind; } diff --git a/packages/headless/src/app/ssr-engine/types/common.ts b/packages/headless/src/app/ssr-engine/types/common.ts index 0a0d9eb757b..bfcd2327bce 100644 --- a/packages/headless/src/app/ssr-engine/types/common.ts +++ b/packages/headless/src/app/ssr-engine/types/common.ts @@ -92,7 +92,7 @@ export interface ControllerDefinitionWithProps< * @param props - The controller properties. * @returns The controller. */ - buildWithProps(engine: TEngine, props: TProps): TController; + buildWithProps(engine: TEngine, props?: TProps): TController; } export type ControllerDefinition< diff --git a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts index e65140c7739..46e661cb182 100644 --- a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts @@ -3,6 +3,7 @@ import { createControllerWithKind, Kind, } from '../../../../app/commerce-ssr-engine/types/kind.js'; +import {MissingControllerProps} from '../../../../utils/errors.js'; import {Cart, buildCart, CartInitialState} from './headless-cart.js'; export type {CartState, CartItem, CartProps} from './headless-cart.js'; @@ -30,6 +31,9 @@ export function defineCart(): CartDefinition { standalone: true, recommendation: true, buildWithProps: (engine, props) => { + if (props === undefined) { + throw new MissingControllerProps(Kind.Cart); + } const controller = buildCart(engine, {initialState: props.initialState}); return createControllerWithKind(controller, Kind.Cart); }, diff --git a/packages/headless/src/controllers/commerce/context/headless-context.ssr.test.ts b/packages/headless/src/controllers/commerce/context/headless-context.ssr.test.ts new file mode 100644 index 00000000000..99f97514e4b --- /dev/null +++ b/packages/headless/src/controllers/commerce/context/headless-context.ssr.test.ts @@ -0,0 +1,63 @@ +import {SSRCommerceEngine} from '../../../app/commerce-ssr-engine/factories/build-factory.js'; +import {buildMockCommerceState} from '../../../test/mock-commerce-state.js'; +import {buildMockSSRCommerceEngine} from '../../../test/mock-engine-v2.js'; +import {MissingControllerProps} from '../../../utils/errors.js'; +import {buildContext, ContextOptions, Context} from './headless-context.js'; +import {ContextDefinition, defineContext} from './headless-context.ssr.js'; + +vi.mock('./headless-context'); +const buildContextMock = vi.mocked(buildContext); + +describe('define commerce context', () => { + const options: ContextOptions = { + language: 'en', + country: 'us', + currency: 'USD', + view: { + url: 'https://example.org', + }, + }; + let contextDefinition: ContextDefinition; + + beforeEach(() => { + buildContextMock.mockReturnValue({} as Context); + contextDefinition = defineContext(); + }); + + afterEach(() => { + buildContextMock.mockClear(); + }); + + it('defineContext returns the proper type', () => { + expect(contextDefinition).toMatchObject({ + buildWithProps: expect.any(Function), + listing: true, + search: true, + standalone: true, + recommendation: true, + }); + }); + + it('buildWithProps should pass its parameters to the buildContext', () => { + const engine: SSRCommerceEngine = buildMockSSRCommerceEngine({ + ...buildMockCommerceState(), + commerceContext: {...options}, + }); + + contextDefinition.buildWithProps(engine, options); + + expect(buildContextMock).toBeCalledWith(engine, {options}); + }); + + it('should throw when props is undefined', () => { + const engine: SSRCommerceEngine = buildMockSSRCommerceEngine({ + ...buildMockCommerceState(), + commerceContext: {...options}, + }); + const props = undefined as unknown as ContextOptions; + + expect(() => { + contextDefinition.buildWithProps(engine, props); + }).toThrow(MissingControllerProps); + }); +}); diff --git a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts index 589b9b61750..6d892598181 100644 --- a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts @@ -3,6 +3,7 @@ import { createControllerWithKind, Kind, } from '../../../app/commerce-ssr-engine/types/kind.js'; +import {MissingControllerProps} from '../../../utils/errors.js'; import { Context, buildContext, @@ -32,6 +33,9 @@ export function defineContext(): ContextDefinition { standalone: true, recommendation: true, buildWithProps: (engine, props) => { + if (props === undefined) { + throw new MissingControllerProps(Kind.Context); + } const controller = buildContext(engine, {options: props}); return createControllerWithKind(controller, Kind.Context); }, diff --git a/packages/headless/src/controllers/context/headless-context.ssr.test.ts b/packages/headless/src/controllers/context/headless-context.ssr.test.ts index 5c490efae8f..b91f4a4b5c7 100644 --- a/packages/headless/src/controllers/context/headless-context.ssr.test.ts +++ b/packages/headless/src/controllers/context/headless-context.ssr.test.ts @@ -1,21 +1,19 @@ import {SSRSearchEngine} from '../../app/search-engine/search-engine.ssr.js'; -import {ControllerDefinitionWithProps} from '../../app/ssr-engine/types/common.js'; import {buildMockSSRSearchEngine} from '../../test/mock-engine-v2.js'; import {createMockState} from '../../test/mock-state.js'; -import {Context, buildContext} from './headless-context.js'; -import {ContextProps, defineContext} from './headless-context.ssr.js'; +import {MissingControllerProps} from '../../utils/errors.js'; +import {buildContext} from './headless-context.js'; +import { + ContextDefinition, + ContextProps, + defineContext, +} from './headless-context.ssr.js'; vi.mock('./headless-context'); const buildContextMock = vi.mocked(buildContext); -type contextDefinitionType = ControllerDefinitionWithProps< - SSRSearchEngine, - Context, - ContextProps ->; - describe('define context', () => { - let contextDefinition: contextDefinitionType; + let contextDefinition: ContextDefinition; beforeEach(() => { contextDefinition = defineContext(); @@ -23,12 +21,12 @@ describe('define context', () => { }); it('defineContext returns the proper type', () => { - expect(contextDefinition).toMatchObject({ + expect(contextDefinition).toMatchObject({ buildWithProps: expect.any(Function), }); }); - it("buildWithProps should pass it's parameters to the buildContext", () => { + it('buildWithProps should pass its parameters to the buildContext', () => { const engine: SSRSearchEngine = buildMockSSRSearchEngine(createMockState()); const props: ContextProps = {} as unknown as ContextProps; @@ -38,4 +36,13 @@ describe('define context', () => { expect(buildContextMock).toBeCalledWith(engine, props); }); + + it('should throw when props is undefined', () => { + const engine: SSRSearchEngine = buildMockSSRSearchEngine(createMockState()); + const props: ContextProps = undefined as unknown as ContextProps; + + expect(() => { + contextDefinition.buildWithProps(engine, props); + }).toThrow(MissingControllerProps); + }); }); diff --git a/packages/headless/src/controllers/context/headless-context.ssr.ts b/packages/headless/src/controllers/context/headless-context.ssr.ts index 60564ef458a..4026f7c57f9 100644 --- a/packages/headless/src/controllers/context/headless-context.ssr.ts +++ b/packages/headless/src/controllers/context/headless-context.ssr.ts @@ -1,5 +1,6 @@ import {SearchEngine} from '../../app/search-engine/search-engine.js'; import {ControllerDefinitionWithProps} from '../../app/ssr-engine/types/common.js'; +import {MissingControllerProps} from '../../utils/errors.js'; import {ContextProps} from '../core/context/headless-core-context.js'; import {Context, buildContext} from './headless-context.js'; @@ -16,7 +17,11 @@ export interface ContextDefinition * */ export function defineContext(): ContextDefinition { return { - buildWithProps: (engine, props) => - buildContext(engine, {initialState: props.initialState}), + buildWithProps: (engine, props) => { + if (props === undefined) { + throw new MissingControllerProps('Context'); + } + return buildContext(engine, {initialState: props.initialState}); + }, }; } diff --git a/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.ssr.ts b/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.ssr.ts index db49f1d36f0..1e8bb105ab0 100644 --- a/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.ssr.ts +++ b/packages/headless/src/controllers/search-parameter-manager/headless-search-parameter-manager.ssr.ts @@ -14,7 +14,7 @@ import {queryReducer as query} from '../../features/query/query-slice.js'; import {sortCriteriaReducer as sortCriteria} from '../../features/sort-criteria/sort-criteria-slice.js'; import {staticFilterSetReducer as staticFilterSet} from '../../features/static-filter-set/static-filter-set-slice.js'; import {tabSetReducer as tabSet} from '../../features/tab-set/tab-set-slice.js'; -import {loadReducerError} from '../../utils/errors.js'; +import {loadReducerError, MissingControllerProps} from '../../utils/errors.js'; import {advancedSearchQueriesReducer as advancedSearchQueries} from './../../features/advanced-search-queries/advanced-search-queries-slice.js'; import { SearchParameterManager, @@ -44,6 +44,9 @@ export interface SearchParameterManagerDefinition export function defineSearchParameterManager(): SearchParameterManagerDefinition { return { buildWithProps: (engine, props) => { + if (props === undefined) { + throw new MissingControllerProps('SearchParameterManager'); + } if (!loadSearchParameterManagerReducers(engine)) { throw loadReducerError; } diff --git a/packages/headless/src/controllers/url-manager/headless-url-manager.ssr.ts b/packages/headless/src/controllers/url-manager/headless-url-manager.ssr.ts index 3663e59024b..878987f823a 100644 --- a/packages/headless/src/controllers/url-manager/headless-url-manager.ssr.ts +++ b/packages/headless/src/controllers/url-manager/headless-url-manager.ssr.ts @@ -1,5 +1,6 @@ import {SearchEngine} from '../../app/search-engine/search-engine.js'; import {ControllerDefinitionWithProps} from '../../app/ssr-engine/types/common.js'; +import {MissingControllerProps} from '../../utils/errors.js'; import { UrlManager, UrlManagerInitialState, @@ -25,6 +26,10 @@ export const defineUrlManager = (): ControllerDefinitionWithProps< UrlManager, UrlManagerBuildProps > => ({ - buildWithProps: (engine, props) => - buildUrlManager(engine, {initialState: props.initialState}), + buildWithProps: (engine, props) => { + if (props === undefined) { + throw new MissingControllerProps('UrlManager'); + } + return buildUrlManager(engine, {initialState: props.initialState}); + }, }); diff --git a/packages/headless/src/utils/errors.ts b/packages/headless/src/utils/errors.ts index a1998dcc663..b39c2415545 100644 --- a/packages/headless/src/utils/errors.ts +++ b/packages/headless/src/utils/errors.ts @@ -22,6 +22,15 @@ export class InvalidControllerDefinition extends Error { } } +export class MissingControllerProps extends Error { + constructor(controller: string) { + super(); + this.name = 'MissingControllerProps'; + this.message = `${controller} props are required but were undefined. Ensure they are included when calling \`fetchStaticState\` or \`hydrateStaticState\`.`; + // + '\nSee [TODO: add link to fetchStaticState example] for more information.'; + } +} + export class MultipleRecommendationError extends Error { constructor(slotId: string) { super();