Skip to content

Commit

Permalink
test(headless SSR): add unit tests for all solution type factories (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
y-lakhdar authored Dec 17, 2024
1 parent 0a3e0d5 commit 84f933b
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {Mock, describe, it, expect, vi} from 'vitest';
import {defineCart} from '../../../controllers/commerce/context/cart/headless-cart.ssr.js';
import {defineProductList} from '../../../controllers/commerce/product-list/headless-product-list.ssr.js';
import {defineRecommendations} from '../../../controllers/commerce/recommendations/headless-recommendations.ssr.js';
import {defineSearchBox} from '../../../controllers/commerce/search-box/headless-search-box.ssr.js';
import {getSampleCommerceEngineConfiguration} from '../../commerce-engine/commerce-engine-configuration.js';
import * as commerceEngine from '../../commerce-engine/commerce-engine.js';
import {CommerceEngineOptions} from '../../commerce-engine/commerce-engine.js';
Expand All @@ -15,27 +18,33 @@ describe('buildFactory', () => {
debug: vi.fn(),
};

const engineOptions: CommerceEngineOptions = {
const mockEngineOptions: CommerceEngineOptions = {
configuration: getSampleCommerceEngineConfiguration(),
navigatorContextProvider: vi.fn(),
};

const mockControllerDefinitions = {};
const mockEmptyDefinition = {};

beforeEach(() => {
const actualImplementation = commerceEngine.buildCommerceEngine;
vi.spyOn(commerceEngine, 'buildCommerceEngine').mockImplementation(
actualImplementation
);
vi.spyOn(commerceEngine, 'buildCommerceEngine');
(buildLogger as Mock).mockReturnValue(mockLogger);
});

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

it('should not warn if navigatorContextProvider is present', async () => {
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.listing);

await build();

expect(mockLogger.warn).not.toHaveBeenCalled();
});

it('should warn if navigatorContextProvider is missing', async () => {
const factory = buildFactory(mockControllerDefinitions, {
const factory = buildFactory(mockEmptyDefinition, {
configuration: getSampleCommerceEngineConfiguration(),
});
const build = factory(SolutionType.listing);
Expand All @@ -48,18 +57,18 @@ describe('buildFactory', () => {
});

it('should throw an error for unsupported solution type', async () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory('unsupported' as SolutionType);

await expect(build()).rejects.toThrow('Unsupported solution type');
});

describe('when building for standalone', () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.standalone);

it('should build SSRCommerceEngine with standalone solution type', async () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.standalone);
const result = await build();

Expand All @@ -77,11 +86,11 @@ describe('buildFactory', () => {
});

describe('when building for search', () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.search);

it('should build SSRCommerceEngine with search solution type', async () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.search);
const result = await build();

Expand All @@ -99,20 +108,74 @@ describe('buildFactory', () => {
});

describe('when building for listing', () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const build = factory(SolutionType.listing);

it('should build SSRCommerceEngine with listing solution type', async () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.listing);
const result = await build();

expect(result.engine).toBeDefined();
expect(result.controllers).toBeDefined();
});

it('should return static state from build result with product and searchbox controllers', async () => {
const factory = buildFactory(
{
products: defineProductList(),
searchBox: defineSearchBox(),
cart: defineCart(),
},
mockEngineOptions
);
await factory(SolutionType.listing)({
controllers: {cart: {initialState: {items: []}}},
});

const {controllers} = await factory(SolutionType.listing)({
controllers: {cart: {initialState: {items: []}}},
});

expect(Object.keys(controllers)).toHaveLength(2);
expect(controllers.cart).not.toBeUndefined();
expect(controllers.products).not.toBeUndefined();
// @ts-expect-error SearchBox is not a listing controller
expect(controllers.searchBox).toBeUndefined();
});

it('should return static state from build result without the listing controller', async () => {
const factory = buildFactory(
{
products: defineProductList({listing: false}),
searchBox: defineSearchBox(),
cart: defineCart(),
},
mockEngineOptions
);
await factory(SolutionType.listing)({
controllers: {cart: {initialState: {items: []}}},
});

const {controllers} = await factory(SolutionType.listing)({
controllers: {cart: {initialState: {items: []}}},
});

expect(Object.keys(controllers)).toHaveLength(1);
expect(controllers.cart).not.toBeUndefined();
// @ts-expect-error Products is disabled for listing
expect(controllers.products).toBeUndefined();
// @ts-expect-error SearchBox is not a listing controller
expect(controllers.searchBox).toBeUndefined();
});

it('should always add a single middleware', async () => {
await build();
const factory = buildFactory(
{
products: defineProductList(),
searchBox: defineSearchBox(),
},
mockEngineOptions
);
await factory(SolutionType.listing)();

expect(
(commerceEngine.buildCommerceEngine as Mock).mock.calls[0][0]
.middlewares
Expand All @@ -134,11 +197,11 @@ describe('buildFactory', () => {
}),
};

const factory = buildFactory(controllerDefinition, engineOptions);
const factory = buildFactory(controllerDefinition, mockEngineOptions);
const build = factory(SolutionType.recommendation);

it('should build SSRCommerceEngine with recommendation solution type', async () => {
const factory = buildFactory(mockControllerDefinitions, engineOptions);
const factory = buildFactory(mockEmptyDefinition, mockEngineOptions);
const build = factory(SolutionType.recommendation);
const result = await build();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {Mock} from 'vitest';
import {Mock, MockInstance} from 'vitest';
import {defineRecommendations} from '../../../controllers/commerce/recommendations/headless-recommendations.ssr.js';
import {buildMockCommerceState} from '../../../test/mock-commerce-state.js';
import {buildMockSSRCommerceEngine} from '../../../test/mock-engine-v2.js';
import {getSampleCommerceEngineConfiguration} from '../../commerce-engine/commerce-engine-configuration.js';
import {buildLogger} from '../../logger.js';
import {
InferControllersMapFromDefinition,
SolutionType,
} from '../types/common.js';
import {CommerceControllerDefinitionsMap} from '../types/core-engine.js';
import type {CommerceEngineDefinitionOptions} from './build-factory.js';
import * as buildFactory from './build-factory.js';
import {fetchRecommendationStaticStateFactory} from './recommendation-static-state-factory.js';

vi.mock('../../logger.js');

describe('fetchRecommendationStaticStateFactory', () => {
const mockLogger = {
warn: vi.fn(),
debug: vi.fn(),
};
let engineSpy: MockInstance;
const mockEngine = buildMockSSRCommerceEngine(buildMockCommerceState());

const createEngineOptions = () => ({
configuration: getSampleCommerceEngineConfiguration(),
Expand All @@ -34,51 +34,26 @@ describe('fetchRecommendationStaticStateFactory', () => {
});

beforeEach(() => {
(buildLogger as Mock).mockReturnValue(mockLogger);
});

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

it('should log a warning if navigatorContextProvider is not set', async () => {
const controllerDefinitions: CommerceControllerDefinitionsMap = {};
const options: CommerceEngineDefinitionOptions<CommerceControllerDefinitionsMap> =
{configuration: getSampleCommerceEngineConfiguration()};

const factory = fetchRecommendationStaticStateFactory(
controllerDefinitions,
options
engineSpy = vi.spyOn(buildFactory, 'buildFactory').mockReturnValue(
() =>
<T extends SolutionType>() =>
Promise.resolve({
engine: mockEngine,
controllers: {} as InferControllersMapFromDefinition<
CommerceControllerDefinitionsMap,
T
>,
})
);

await factory();

expect(mockLogger.warn).toHaveBeenCalledTimes(1);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Missing navigator context in server-side code')
);
(mockEngine.waitForRequestCompletedAction as Mock).mockReturnValue([]);
});

it('should not log a warning if navigatorContextProvider is set', async () => {
const controllerDefinitions: CommerceControllerDefinitionsMap = {};
const options: CommerceEngineDefinitionOptions<CommerceControllerDefinitionsMap> =
{
configuration: getSampleCommerceEngineConfiguration(),
navigatorContextProvider: vi.fn(),
};

const factory = fetchRecommendationStaticStateFactory(
controllerDefinitions,
options
);

await factory();

expect(mockLogger.warn).toHaveBeenCalledTimes(0);
afterEach(() => {
vi.clearAllMocks();
});

it('should call buildFactory with the correct parameters', async () => {
const spy = vi.spyOn(buildFactory, 'buildFactory');
const controllerDefinitions = createControllerDefinitions();
const options = createEngineOptions();

Expand All @@ -89,24 +64,6 @@ describe('fetchRecommendationStaticStateFactory', () => {

await factory({controllers: {}});

expect(spy).toHaveBeenCalledWith(controllerDefinitions, options);
});

it('should return static state from build result', async () => {
const controllerDefinitions = createControllerDefinitions();
const options = createEngineOptions();

const factory = fetchRecommendationStaticStateFactory(
controllerDefinitions,
options
);

const {controllers} = await factory({
controllers: {},
});

expect(Object.keys(controllers)).toHaveLength(2);
expect(controllers.popularBought).toHaveProperty('state');
expect(controllers.popularViewed).toHaveProperty('state');
expect(engineSpy).toHaveBeenCalledWith(controllerDefinitions, options);
});
});
Loading

0 comments on commit 84f933b

Please sign in to comment.