diff --git a/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue b/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue index e30dba0c8..0c0429689 100644 --- a/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue +++ b/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue @@ -183,3 +183,180 @@ } }); + + +## Events + +This component emits no events. + +## See it in action + + + +:::warning Backend microservice required To use this component, the QuerySignals microservice +must be implemented. ::: + + + +Usually, this component is going to be used together with the `ResultsList` one. Related prompts +groups will be inserted between the results, guiding users to discover new searches directly from +the results list. + +```vue live + + + +``` + +### Play with the index that related prompts groups are inserted at + +The component allows to customise where are the related prompts groups inserted. In the following +example, the first group of related prompts will be inserted at the index `48` (`offset`), and then +a second group will be inserted at index `120` because of the `frequency` prop configured to `72`. +Finally, a third group will be inserted at index `192`. Because `maxGroups` is configured to `3`, no +more groups will be inserted. Each one of this groups will have up to `6` related prompts +(`maxRelatedPromptsPerGroup`). + +```vue live + + + +``` + +### Showing/hiding first related prompts group when no more items + +By default, the first related prompts group will be inserted when the total number of results is +smaller than the offset, but this behavior can be deactivated by setting the `showOnlyAfterOffset` +to `true`. + +```vue live + + + +``` + +### Customise the layout of the component + +This component will render by default the `id` of each search item, both the injected, and for the +groups of related prompts generated, but the common case is to integrate it with another layout +component, for example the `BaseGrid`. To do so, you can use the `default` slot + +```vue + + + +``` + diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts new file mode 100644 index 000000000..79efe65be --- /dev/null +++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts @@ -0,0 +1,118 @@ +import { mount } from '@vue/test-utils'; +import { Store } from 'vuex'; +import { getRelatedPromptsStub } from '../../../../__stubs__'; +import { getMockedAdapter, installNewXPlugin } from '../../../../__tests__/utils'; +import { SafeStore } from '../../../../store/__tests__/utils'; +import { + RelatedPromptsActions, + RelatedPromptsGetters, + RelatedPromptsMutations, + RelatedPromptsState +} from '../types'; +import { relatedPromptsXStoreModule } from '../module'; +import { resetRelatedPromptsStateWith } from './utils'; + +describe('testing related prompts module actions', () => { + const mockedRelatedPrompts = getRelatedPromptsStub(); + + const adapter = getMockedAdapter({ + relatedPrompts: { relatedPrompts: mockedRelatedPrompts } + }); + + const store: SafeStore< + RelatedPromptsState, + RelatedPromptsGetters, + RelatedPromptsMutations, + RelatedPromptsActions + > = new Store(relatedPromptsXStoreModule as any); + mount( + {}, + { + global: { + plugins: [installNewXPlugin({ adapter, store })] + } + } + ); + + beforeEach(() => { + resetRelatedPromptsStateWith(store); + }); + + describe('fetchRelatedPrompts', () => { + it('should return related prompts', async () => { + resetRelatedPromptsStateWith(store, { + query: 'honeyboo' + }); + + const relatedPrompts = await store.dispatch('fetchRelatedPrompts', store.getters.request); + expect(relatedPrompts).toEqual(mockedRelatedPrompts); + }); + + it('should return `null` if there is not request', async () => { + const relatedPrompts = await store.dispatch('fetchRelatedPrompts', store.getters.request); + expect(relatedPrompts).toBeNull(); + }); + }); + + describe('fetchAndSaveRelatedPrompts', () => { + it('should request and store related prompts in the state', async () => { + resetRelatedPromptsStateWith(store, { + query: 'honeyboo' + }); + + const actionPromise = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request); + expect(store.state.status).toEqual('loading'); + await actionPromise; + expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts); + expect(store.state.status).toEqual('success'); + }); + + it('should not clear related prompts in the state if the query is empty', async () => { + resetRelatedPromptsStateWith(store, { relatedPrompts: mockedRelatedPrompts }); + + await store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request); + expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts); + }); + + it('should cancel the previous request if it is not yet resolved', async () => { + resetRelatedPromptsStateWith(store, { query: 'steak' }); + const initialRelatedPrompts = store.state.relatedPrompts; + adapter.relatedPrompts.mockResolvedValueOnce({ + relatedPrompts: mockedRelatedPrompts.slice(0, 1) + }); + + const firstRequest = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request); + const secondRequest = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request); + + await firstRequest; + expect(store.state.status).toEqual('loading'); + expect(store.state.relatedPrompts).toBe(initialRelatedPrompts); + await secondRequest; + expect(store.state.status).toEqual('success'); + expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts); + }); + + it('should set the status to error when it fails', async () => { + resetRelatedPromptsStateWith(store, { query: 'milk' }); + adapter.relatedPrompts.mockRejectedValueOnce('Generic error'); + const relatedPrompts = store.state.relatedPrompts; + await store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request); + + expect(store.state.relatedPrompts).toBe(relatedPrompts); + expect(store.state.status).toEqual('error'); + }); + }); + + describe('cancelFetchAndSaveRelatedPrompts', () => { + it('should cancel the request and do not modify the stored related prompts', async () => { + resetRelatedPromptsStateWith(store, { query: 'honeyboo' }); + const previousRelatedPrompts = store.state.relatedPrompts; + await Promise.all([ + store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request), + store.dispatch('cancelFetchAndSaveRelatedPrompts') + ]); + expect(store.state.relatedPrompts).toEqual(previousRelatedPrompts); + expect(store.state.status).toEqual('success'); + }); + }); +}); diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts new file mode 100644 index 000000000..304ea29c0 --- /dev/null +++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts @@ -0,0 +1,37 @@ +import { RelatedPromptsRequest } from '@empathyco/x-types'; +import { map } from '@empathyco/x-utils'; +import { Store } from 'vuex'; +import { relatedPromptsXStoreModule } from '../module'; +import { RelatedPromptsState } from '../types'; +import { resetRelatedPromptsStateWith } from './utils'; + +describe('testing related prompts module getters', () => { + const gettersKeys = map(relatedPromptsXStoreModule.getters, getter => getter); + const store: Store = new Store(relatedPromptsXStoreModule as any); + + beforeEach(() => { + resetRelatedPromptsStateWith(store); + }); + + describe(`${gettersKeys.request} getter`, () => { + it('should return a request object if there is a query', () => { + resetRelatedPromptsStateWith(store, { + query: 'queso', + params: { + catalog: 'es' + } + }); + + expect(store.getters[gettersKeys.request]).toEqual({ + query: 'queso', + extraParams: { + catalog: 'es' + } + }); + }); + + it('should return null when there is not query', () => { + expect(store.getters[gettersKeys.request]).toBeNull(); + }); + }); +}); diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts new file mode 100644 index 000000000..cf4e0254f --- /dev/null +++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts @@ -0,0 +1,21 @@ +import { DeepPartial } from '@empathyco/x-utils'; +import { Store } from 'vuex'; +import { resetStoreModuleState } from '../../../../__tests__/utils'; +import { relatedPromptsXStoreModule } from '../module'; +import { RelatedPromptsState } from '../types'; + +/** + * Reset related prompt module state with its original state and the partial state passes as + * parameter. + * + * @param store - Related prompt store state. + * @param state - Partial related prompt store state to be replaced. + * + * @internal + */ +export function resetRelatedPromptsStateWith( + store: Store, + state?: DeepPartial +): void { + resetStoreModuleState(store, relatedPromptsXStoreModule.state(), state); +}