diff --git a/packages/x-components/src/__stubs__/experience-controls-stubs.factory.ts b/packages/x-components/src/__stubs__/experience-controls-stubs.factory.ts new file mode 100644 index 000000000..ee1f4f2f4 --- /dev/null +++ b/packages/x-components/src/__stubs__/experience-controls-stubs.factory.ts @@ -0,0 +1,24 @@ +import { ExperienceControlsResponse } from '@empathyco/x-types'; + +/** + * Creates a an experience controls response stub. + * + * @returns An experience controls stub. + * + * @internal + */ +export function getExperienceControlsStub(): ExperienceControlsResponse { + return createExperienceControlsStub(); +} + +/** + * Creates an experience controls response. + * + * @returns An experience controls response. + */ +export function createExperienceControlsStub(): ExperienceControlsResponse { + return { + controls: { numberOfCarousels: 10, resultsPerCarousels: 21 }, + events: { ColumnsNumberProvided: 6 } + }; +} diff --git a/packages/x-components/src/__tests__/utils.ts b/packages/x-components/src/__tests__/utils.ts index 1f2bcadc5..0e00adffc 100644 --- a/packages/x-components/src/__tests__/utils.ts +++ b/packages/x-components/src/__tests__/utils.ts @@ -182,7 +182,7 @@ function mergeStates( } /** - * Makes a clean install of the's the {@link XPlugin} into the passed Vue object. + * Makes a clean install of the {@link XPlugin} into the passed Vue object. * This also resets the bus, and all the hardcoded dependencies of the XPlugin. * * @param options - The options for installing the {@link XPlugin}. The @@ -221,3 +221,29 @@ export function createXModule< ): XModule> { return xModule; } + +/** + * Mocks a `fetch` API call. + * + * @param response - The expected response resolved by calling `fetch()`. + * @returns A Promise object. + * + * @internal + */ +export function getFetchMock( + response: unknown +): (url: string, params: RequestInit) => Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return _url => { + return new Promise(resolve => { + setTimeout(() => { + resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(response), + text: () => Promise.resolve(JSON.stringify(response)) + } as Response); + }); + }); + }; +} diff --git a/packages/x-components/src/x-modules/experience-controls/components/__tests__/experience-controls.spec.ts b/packages/x-components/src/x-modules/experience-controls/components/__tests__/experience-controls.spec.ts new file mode 100644 index 000000000..d3ac544a2 --- /dev/null +++ b/packages/x-components/src/x-modules/experience-controls/components/__tests__/experience-controls.spec.ts @@ -0,0 +1,61 @@ +import { mount, Wrapper } from '@vue/test-utils'; +import Vue from 'vue'; +import { XPlugin } from '../../../../plugins/index'; +import { installNewXPlugin } from '../../../../__tests__/utils'; +import { experienceControlsXModule } from '../../x-module'; +import { getXComponentXModuleName, isXComponent } from '../../../../components/index'; +import ExperienceControls from '../experience-controls.vue'; + +function renderExperienceControls(): RenderExperienceControlsApi { + const [, localVue] = installNewXPlugin(); + XPlugin.registerXModule(experienceControlsXModule); + + const wrapper = mount(ExperienceControls, { + localVue + }); + + return { + wrapper + }; +} + +describe('testing experience controls component', () => { + it('is an XComponent which has an XModule', () => { + const { wrapper } = renderExperienceControls(); + expect(isXComponent(wrapper.vm)).toEqual(true); + expect(getXComponentXModuleName(wrapper.vm)).toEqual('experienceControls'); + }); + + // eslint-disable-next-line max-len + it('listens to the event ExperienceControlsEventsChanged and emits the events on the payload', () => { + const { wrapper } = renderExperienceControls(); + + const eventsFromExperienceControls = { + ExtraParamsProvided: { + warehouse: 'Magrathea' + }, + SortChanged: 'price:desc' + }; + + const extraParamsProvidedListener = jest.fn(); + wrapper.vm.$x.on('ExtraParamsProvided').subscribe(extraParamsProvidedListener); + + const sortChangedListener = jest.fn(); + wrapper.vm.$x.on('SortChanged').subscribe(sortChangedListener); + + wrapper.vm.$x.emit('ExperienceControlsEventsChanged', eventsFromExperienceControls); + + expect(extraParamsProvidedListener).toHaveBeenCalledTimes(1); + expect(extraParamsProvidedListener).toHaveBeenCalledWith({ + warehouse: 'Magrathea' + }); + + expect(sortChangedListener).toHaveBeenCalledTimes(1); + expect(sortChangedListener).toHaveBeenCalledWith('price:desc'); + }); +}); + +interface RenderExperienceControlsApi { + /** The wrapper for the experience controls component. */ + wrapper: Wrapper; +} diff --git a/packages/x-components/src/x-modules/experience-controls/store/__tests__/actions.spec.ts b/packages/x-components/src/x-modules/experience-controls/store/__tests__/actions.spec.ts new file mode 100644 index 000000000..0c058d79d --- /dev/null +++ b/packages/x-components/src/x-modules/experience-controls/store/__tests__/actions.spec.ts @@ -0,0 +1,87 @@ +import Vuex from 'vuex'; +import { createLocalVue } from '@vue/test-utils'; +import { getMockedAdapter, installNewXPlugin } from '../../../../__tests__/utils'; +import { getExperienceControlsStub } from '../../../../__stubs__/experience-controls-stubs.factory'; +import { createExperienceControlsStore, resetExperienceControlsStateWith } from './utils'; + +describe('testing experience controls module actions', () => { + const mockedResponse = getExperienceControlsStub(); + + const adapter = getMockedAdapter({ + experienceControls: mockedResponse + }); + + const localVue = createLocalVue(); + localVue.config.productionTip = false; // Silent production console messages. + localVue.use(Vuex); + const store = createExperienceControlsStore(); + installNewXPlugin({ adapter, store }, localVue); + + beforeEach(() => { + resetExperienceControlsStateWith(store); + }); + + describe('fetchControls', () => { + it('should return experience controls response', async () => { + const experienceControls = await store.dispatch( + 'fetchExperienceControlsResponse', + store.getters.experienceControlsRequest + ); + expect(experienceControls).toEqual(mockedResponse); + }); + }); + + describe('fetchAndSaveControls', () => { + it('should request and store controls and events in the state', async () => { + const actionPromise = store.dispatch( + 'fetchAndSaveExperienceControlsResponse', + store.getters.experienceControlsRequest + ); + expect(store.state.status).toEqual('loading'); + await actionPromise; + + expect(store.state.controls).toEqual(mockedResponse.controls); + expect(store.state.events).toEqual(mockedResponse.events); + expect(store.state.status).toEqual('success'); + }); + + it('should cancel the previous request if it is not yet resolved', async () => { + const initialExperienceControls = store.state.controls; + adapter.experienceControls.mockResolvedValueOnce(mockedResponse); + + const firstRequest = store.dispatch( + 'fetchAndSaveExperienceControlsResponse', + store.getters.experienceControlsRequest + ); + const secondRequest = store.dispatch( + 'fetchAndSaveExperienceControlsResponse', + store.getters.experienceControlsRequest + ); + + await firstRequest; + expect(store.state.status).toEqual('loading'); + expect(store.state.controls).toBe(initialExperienceControls); + await secondRequest; + expect(store.state.status).toEqual('success'); + expect(store.state.controls).toEqual(mockedResponse.controls); + }); + }); + + describe('cancelFetchAndSaveControls', () => { + it('should cancel the request and do not modify the stored controls', async () => { + resetExperienceControlsStateWith(store, { + controls: { numberOfCarousels: 20, resultsPerCarousels: 6 } + }); + const previousControls = store.state.controls; + await Promise.all([ + store.dispatch( + 'fetchAndSaveExperienceControlsResponse', + store.getters.experienceControlsRequest + ), + store.dispatch('cancelFetchAndSaveControls') + ]); + expect(store.state.controls).toEqual(previousControls); + expect(store.state.status).toEqual('success'); + }); + }); +}); diff --git a/packages/x-components/src/x-modules/experience-controls/store/__tests__/getters.spec.ts b/packages/x-components/src/x-modules/experience-controls/store/__tests__/getters.spec.ts new file mode 100644 index 000000000..3cd5e9c8d --- /dev/null +++ b/packages/x-components/src/x-modules/experience-controls/store/__tests__/getters.spec.ts @@ -0,0 +1,31 @@ +import Vuex from 'vuex'; +import { ExperienceControlsRequest } from '@empathyco/x-types'; +import { createLocalVue } from '@vue/test-utils'; +import { createExperienceControlsStore, resetExperienceControlsStateWith } from './utils'; + +describe('testing experience controls module getters', () => { + const localVue = createLocalVue(); + localVue.config.productionTip = false; + localVue.use(Vuex); + const store = createExperienceControlsStore(); + + beforeEach(() => { + resetExperienceControlsStateWith(store); + }); + + describe(`request getter`, () => { + it('should return a request object', () => { + resetExperienceControlsStateWith(store, { + params: { + store: 'es' + } + }); + + expect(store.getters.experienceControlsRequest).toEqual({ + extraParams: { + store: 'es' + } + }); + }); + }); +}); diff --git a/packages/x-components/src/x-modules/experience-controls/store/__tests__/utils.ts b/packages/x-components/src/x-modules/experience-controls/store/__tests__/utils.ts new file mode 100644 index 000000000..f8b66f8f2 --- /dev/null +++ b/packages/x-components/src/x-modules/experience-controls/store/__tests__/utils.ts @@ -0,0 +1,45 @@ +import { DeepPartial } from '@empathyco/x-utils'; +import { Store } from 'vuex'; +import { resetStoreModuleState } from '../../../../__tests__/utils'; +import { experienceControlsXStoreModule } from '../module'; +import { + ExperienceControlsActions, + ExperienceControlsGetters, + ExperienceControlsMutations, + ExperienceControlsState +} from '../types'; +import { SafeStore } from '../../../../store/__tests__/utils'; + +/** + * Resets the experience controls module state, optionally modifying its default values. + * + * @param store - Experience controls store state. + * @param state - Partial experience controls store state to replace the original one. + * + * @internal + */ +export function resetExperienceControlsStateWith( + store: Store, + state?: DeepPartial +): void { + resetStoreModuleState(store, experienceControlsXStoreModule.state(), state); +} + +/** + * Creates an experience controls store with the state passed as parameter. + * + * @returns Store - The new store created. + * + * @internal + */ +export function createExperienceControlsStore(): Store { + const store: SafeStore< + ExperienceControlsState, + ExperienceControlsGetters, + ExperienceControlsMutations, + ExperienceControlsActions + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + > = new Store(experienceControlsXStoreModule as any); + + return store; +} diff --git a/packages/x-components/src/x-modules/experience-controls/store/module.ts b/packages/x-components/src/x-modules/experience-controls/store/module.ts index b6c760bf2..7daf294de 100644 --- a/packages/x-components/src/x-modules/experience-controls/store/module.ts +++ b/packages/x-components/src/x-modules/experience-controls/store/module.ts @@ -1,7 +1,10 @@ /* eslint-disable max-len */ import Vue from 'vue'; import { setStatus } from '../../../store/utils/status-store.utils'; -import { fetchAndSaveExperienceControlsResponse } from './actions/fetch-and-save-experience-controls.action'; +import { + cancelFetchAndSaveControls, + fetchAndSaveExperienceControlsResponse +} from './actions/fetch-and-save-experience-controls.action'; import { fetchExperienceControlsResponse } from './actions/fetch-experience-controls.action'; import { experienceControlsRequest } from './getters/experience-controls-results-request.getter'; import { ExperienceControlsXStoreModule } from './types'; @@ -36,6 +39,7 @@ export const experienceControlsXStoreModule: ExperienceControlsXStoreModule = { }, actions: { fetchExperienceControlsResponse, - fetchAndSaveExperienceControlsResponse + fetchAndSaveExperienceControlsResponse, + cancelFetchAndSaveControls } }; diff --git a/packages/x-components/src/x-modules/experience-controls/store/types.ts b/packages/x-components/src/x-modules/experience-controls/store/types.ts index 31c5652e7..eb381a8e4 100644 --- a/packages/x-components/src/x-modules/experience-controls/store/types.ts +++ b/packages/x-components/src/x-modules/experience-controls/store/types.ts @@ -82,6 +82,11 @@ export interface ExperienceControlsActions { * @param request - The request to fetch the experience controls. */ fetchAndSaveExperienceControlsResponse(request: ExperienceControlsRequest | null): void; + + /** + * Cancels the {@link ExperienceControlsActions.fetchAndSaveExperienceControlsResponse}. + */ + cancelFetchAndSaveControls: () => void; } /**