diff --git a/packages/x-components/src/plugins/x-emitters.ts b/packages/x-components/src/plugins/x-emitters.ts index c254295e5d..c9502b0621 100644 --- a/packages/x-components/src/plugins/x-emitters.ts +++ b/packages/x-components/src/plugins/x-emitters.ts @@ -31,9 +31,9 @@ export function registerStoreEmitters( selector(store.state.x[name], safeGettersProxy); const emit: ( newValue: XEventPayload, - oldValue: XEventPayload + oldValue?: XEventPayload ) => void = (newValue, oldValue) => { - if (filter(newValue, oldValue)) { + if (filter(newValue, oldValue, store.state.x[name])) { bus.emit(event, newValue, { ...metadata, moduleName: name, oldValue }); } }; diff --git a/packages/x-components/src/store/utils/store-emitters.utils.ts b/packages/x-components/src/store/utils/store-emitters.utils.ts index d2c41153fb..0b0e5c7221 100644 --- a/packages/x-components/src/store/utils/store-emitters.utils.ts +++ b/packages/x-components/src/store/utils/store-emitters.utils.ts @@ -35,7 +35,8 @@ export interface StateSelector; /** - * Checks if the value of the selector has changed. + * Asserts if the event should really be emitted taking into account the new and old values and + * the module state. * * @remarks * This function exist because Vue will not stop reactivity propagation if the observed variable @@ -43,9 +44,10 @@ export interface StateSelector; } diff --git a/packages/x-components/src/views/home/Home.vue b/packages/x-components/src/views/home/Home.vue index 953241aa72..f47669051f 100644 --- a/packages/x-components/src/views/home/Home.vue +++ b/packages/x-components/src/views/home/Home.vue @@ -248,12 +248,17 @@
-

{{ query }} ({{ totalResults }})

+ + {{ `${queryPreviewInfo.query} (${totalResults})` }} +
item.query); + } toggleE2EAdapter(): void { adapterConfig.e2e = !adapterConfig.e2e; diff --git a/packages/x-components/src/x-installer/api/api.types.ts b/packages/x-components/src/x-installer/api/api.types.ts index c44c9deb2d..20396732cc 100644 --- a/packages/x-components/src/x-installer/api/api.types.ts +++ b/packages/x-components/src/x-installer/api/api.types.ts @@ -3,6 +3,7 @@ import { XBus } from '@empathyco/x-bus'; import { DocumentDirection } from '../../plugins/x-plugin.types'; import { XEvent, XEventPayload, XEventsTypes } from '../../wiring/events.types'; import { WireMetadata } from '../../wiring/wiring.types'; +import { QueryPreviewInfo } from '../../x-modules/queries-preview/index'; /** * Interface with the API functions exposes as X @@ -126,17 +127,3 @@ export interface SnippetConfig { * @public */ export type NormalisedSnippetConfig = RequiredProperties; - -/** - * Information to render a query preview with. - * - * @public - */ -export interface QueryPreviewInfo { - /** The query to search for. */ - query: string; - /** An optional title for the container. */ - title?: string; - /** Any other additional information to render the preview with. */ - [extra: string]: unknown; -} diff --git a/packages/x-components/src/x-modules/history-queries/wiring.ts b/packages/x-components/src/x-modules/history-queries/wiring.ts index 2100d45000..3563261866 100644 --- a/packages/x-components/src/x-modules/history-queries/wiring.ts +++ b/packages/x-components/src/x-modules/history-queries/wiring.ts @@ -39,6 +39,16 @@ const wireDispatchWithoutPayload = namespacedWireDispatchWithoutPayload(moduleNa */ export const addQueryToHistoryQueries = wireDispatch('addQueryToHistory'); +/** + * Saves the selectedQueryPreview query into the history queries. + * + * @public + */ +export const addQueryToHistoryQueriesFromPreview = wireDispatch( + 'addQueryToHistory', + ({ eventPayload: { query } }) => query +); + /** * Sets the query of the history queries module. Used for searching into the history queries. * @@ -46,6 +56,17 @@ export const addQueryToHistoryQueries = wireDispatch('addQueryToHistory'); */ export const setHistoryQueriesQuery = wireCommit('setQuery'); +/** + * Sets the query of the history queries module from a selectedQueryPreview's query. + * Used for searching into the history queries. + * + * @public + */ +export const setHistoryQueriesQueryFromPreview = wireCommit( + 'setQuery', + ({ eventPayload: { query } }) => query +); + /** * Sets the history queries state `query` from url. * @@ -169,5 +190,9 @@ export const historyQueriesWiring = createWiring({ }, SearchResponseChanged: { updateHistoryQueriesWithSearchResponse + }, + UserAcceptedAQueryPreview: { + setHistoryQueriesQueryFromPreview, + addQueryToHistoryQueriesFromPreview } }); diff --git a/packages/x-components/src/x-modules/identifier-results/wiring.ts b/packages/x-components/src/x-modules/identifier-results/wiring.ts index bcd958e4ff..6dc9c2eb37 100644 --- a/packages/x-components/src/x-modules/identifier-results/wiring.ts +++ b/packages/x-components/src/x-modules/identifier-results/wiring.ts @@ -39,6 +39,16 @@ const wireDispatchWithoutPayload = namespacedWireDispatchWithoutPayload(moduleNa */ export const setIdentifierResultsQuery = wireDispatch('saveQuery'); +/** + * Sets the identifier-results module query from a selectedQueryPreview's query. + * + * @public + */ +export const setIdentifierResultsQueryFromPreview = wireDispatch( + 'saveQuery', + ({ eventPayload: { query } }) => query +); + /** * Clears the identifier-results module query. * @@ -127,5 +137,9 @@ export const identifierResultsWiring = createWiring({ }, UserClickedOutOfMainModal: { clearIdentifierResultsQuery + }, + UserAcceptedAQueryPreview: { + setIdentifierResultsQueryFromPreview, + saveIdentifierResultsOriginWire } }); diff --git a/packages/x-components/src/x-modules/next-queries/wiring.ts b/packages/x-components/src/x-modules/next-queries/wiring.ts index a6118fb652..567ce949d9 100644 --- a/packages/x-components/src/x-modules/next-queries/wiring.ts +++ b/packages/x-components/src/x-modules/next-queries/wiring.ts @@ -45,6 +45,16 @@ const wireDispatch: NamespacedWireDispatch = namespacedWireDi */ export const setNextQueriesQuery = wireCommit('setQuery'); +/** + * Sets the next queries state `query` with a selectedQueryPreview's query. + * + * @public + */ +export const setNextQueriesQueryFromPreview = wireCommit( + 'setParams', + ({ eventPayload: { query } }) => query +); + /** * Sets the next queries state `query` from url. * @@ -59,6 +69,16 @@ const setUrlParams = wireDispatch('setUrlParams'); */ export const setNextQueriesExtraParams = wireCommit('setParams'); +/** + * Sets the next queries state `params` with a selectedQueryPreview's extraParams. + * + * @public + */ +export const setNextQueriesExtraParamsFromPreview = wireCommit( + 'setParams', + ({ eventPayload: { extraParams } }) => extraParams +); + /** * Requests and stores the next queries. * @@ -129,5 +149,9 @@ export const nextQueriesWiring = createWiring({ }, NextQueryPreviewMountedHook: { fetchAndSaveNextQueryPreviewWire + }, + UserAcceptedAQueryPreview: { + setNextQueriesQueryFromPreview, + setNextQueriesExtraParamsFromPreview } }); diff --git a/packages/x-components/src/x-modules/popular-searches/wiring.ts b/packages/x-components/src/x-modules/popular-searches/wiring.ts index 10b2a6337a..decbfc4cbd 100644 --- a/packages/x-components/src/x-modules/popular-searches/wiring.ts +++ b/packages/x-components/src/x-modules/popular-searches/wiring.ts @@ -12,7 +12,7 @@ import { createWiring } from '../../wiring/wiring.utils'; */ const moduleName = 'popularSearches'; /** - * WireDispatchfor {@link PopularSearchesXModule}. + * WireDispatch for {@link PopularSearchesXModule}. * * @internal */ diff --git a/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-button.spec.ts b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-button.spec.ts new file mode 100644 index 0000000000..be8bcd2c14 --- /dev/null +++ b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-button.spec.ts @@ -0,0 +1,138 @@ +import Vuex, { Store } from 'vuex'; +import { mount, createLocalVue, Wrapper, WrapperArray } from '@vue/test-utils'; +import { DeepPartial, Dictionary } from '@empathyco/x-utils'; +import Vue from 'vue'; +import { RootXStoreState } from '../../../../store/index'; +import { findTestDataById, installNewXPlugin } from '../../../../__tests__/utils'; +import { XPlugin } from '../../../../plugins/index'; +import { queriesPreviewXModule } from '../../x-module'; +import QueryPreviewButton from '../query-preview-button.vue'; +import { QueryPreviewInfo } from '../../store/index'; +import { getXComponentXModuleName, isXComponent } from '../../../../components/index'; + +function renderQueryPreviewButton({ + queryPreviewInfo = { query: 'milk', extraParams: { store: 'Magrathea' } }, + template = `` +}: RenderQueryPreviewButtonOptions = {}): RenderQueryPreviewButtonAPI { + const localVue = createLocalVue(); + localVue.use(Vuex); + + const store = new Store>({}); + installNewXPlugin({ store }, localVue); + XPlugin.registerXModule(queriesPreviewXModule); + + const queryPreviewButtonEmitSpy = jest.fn(); + XPlugin.bus.on('UserAcceptedAQueryPreview').subscribe(queryPreviewButtonEmitSpy); + + const wrapper = mount( + { + components: { QueryPreviewButton }, + template + }, + { + localVue, + store, + propsData: { + queryPreviewInfo + } + } + ).findComponent(QueryPreviewButton); + + const findTestDataByIdInButton = findTestDataById.bind(undefined, wrapper); + + return { + wrapper, + queryPreviewButtonEmitSpy, + queryPreviewInfo, + findTestDataById: findTestDataByIdInButton, + clickQueryPreviewButton: () => + findTestDataByIdInButton('query-preview-button').trigger('click'), + updateExtraParams: async params => { + store.commit('x/queriesPreview/setParams', params); + await localVue.nextTick(); + } + }; +} + +describe('query preview button', () => { + jest.useFakeTimers(); + afterEach(() => { + jest.runAllTimers(); + jest.resetAllMocks(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('is an XComponent which has an XModule', () => { + const { wrapper } = renderQueryPreviewButton(); + expect(isXComponent(wrapper.vm)).toEqual(true); + expect(getXComponentXModuleName(wrapper.vm)).toBe('queriesPreview'); + }); + + it('default slot with the query text of the query preview', () => { + const { wrapper } = renderQueryPreviewButton(); + expect(wrapper.text()).toBe('milk'); + }); + + it('can override the content of the slot', () => { + const { findTestDataById } = renderQueryPreviewButton({ + template: ` + + + + ` + }); + expect(findTestDataById('custom-content')).toBeTruthy(); + }); + + it('sends the `UserAcceptedAQueryPreview` event when the button is clicked', async () => { + const { clickQueryPreviewButton, queryPreviewButtonEmitSpy, updateExtraParams } = + renderQueryPreviewButton(); + + clickQueryPreviewButton(); + expect(queryPreviewButtonEmitSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewButtonEmitSpy).toHaveBeenCalledWith({ + query: 'milk', + extraParams: { + store: 'Magrathea' + } + }); + + await updateExtraParams({ warehouse: 42 }); + + clickQueryPreviewButton(); + expect(queryPreviewButtonEmitSpy).toHaveBeenCalledTimes(2); + expect(queryPreviewButtonEmitSpy).toHaveBeenCalledWith({ + query: 'milk', + extraParams: { + store: 'Magrathea', + warehouse: 42 + } + }); + }); +}); + +interface RenderQueryPreviewButtonOptions { + /** The query preview info to be used in the component. */ + queryPreviewInfo?: QueryPreviewInfo; + /** The template to be rendered. */ + template?: string; +} + +interface RenderQueryPreviewButtonAPI { + /** The wrapper of the rendered component. */ + wrapper: Wrapper; + /** The spy to check if the event was emitted. */ + queryPreviewButtonEmitSpy: jest.Mock; + /** The query preview info to be used in the component. */ + queryPreviewInfo: QueryPreviewInfo; + /** The function to find a data-test by its id. */ + findTestDataById: (id: string) => WrapperArray; + /** Clicks the query preview button. */ + clickQueryPreviewButton: () => void; + /** Updates the extra params of the query preview module. */ + updateExtraParams: (params: Dictionary) => Promise; +} diff --git a/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-list.spec.ts b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-list.spec.ts index fdd5080262..851c4ab5ce 100644 --- a/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-list.spec.ts +++ b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview-list.spec.ts @@ -10,13 +10,14 @@ import { XComponentsAdapterDummy } from '../../../../__tests__/adapter.dummy'; import { getDataTestSelector, installNewXPlugin } from '../../../../__tests__/utils'; import { queriesPreviewXModule } from '../../x-module'; import QueryPreviewList from '../query-preview-list.vue'; +import { QueryPreviewInfo } from '../../store/types'; function renderQueryPreviewList({ template = ` - - {{ query }} - {{results[0].name}} + + {{ queryPreviewInfo.query }} - {{results[0].name}} `, - queries = ['milk'], + queriesPreviewInfo = [{ query: 'milk' }], results = { milk: getResultsStub(1) } }: RenderQueryPreviewListOptions): RenderQueryPreviewListAPI { const localVue = createLocalVue(); @@ -42,7 +43,7 @@ function renderQueryPreviewList({ { localVue, propsData: { - queries + queriesPreviewInfo } } ); @@ -62,7 +63,7 @@ function renderQueryPreviewList({ describe('testing QueryPreviewList', () => { it('renders a list of queries one by one', async () => { const { getQueryPreviewItemWrappers, reRender } = renderQueryPreviewList({ - queries: ['shirt', 'jeans'], + queriesPreviewInfo: [{ query: 'shirt' }, { query: 'jeans' }], results: { shirt: [createResultStub('Cool shirt')], jeans: [createResultStub('Sick jeans')] } }); @@ -87,7 +88,7 @@ describe('testing QueryPreviewList', () => { it('hides queries with no results', async () => { const { getQueryPreviewItemWrappers, reRender } = renderQueryPreviewList({ - queries: ['noResults', 'shoes'], + queriesPreviewInfo: [{ query: 'noResults' }, { query: 'shoes' }], results: { noResults: [], shoes: [createResultStub('Crazy shoes')] } }); @@ -105,7 +106,7 @@ describe('testing QueryPreviewList', () => { it('hides queries that failed', async () => { const { adapter, getQueryPreviewItemWrappers, reRender } = renderQueryPreviewList({ - queries: ['willFail', 'shoes'], + queriesPreviewInfo: [{ query: 'willFail' }, { query: 'shoes' }], results: { willFail: [createResultStub('Will fail')], shoes: [createResultStub('Crazy shoes')] @@ -131,7 +132,7 @@ interface RenderQueryPreviewListOptions { /** The template to render the {@link QueryPreviewList} component. */ template?: string; /** The queries for which preview its results. */ - queries?: string[]; + queriesPreviewInfo?: QueryPreviewInfo[]; /** The results to return from the mocked search endpoint adapter. */ results?: Record; } diff --git a/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview.spec.ts b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview.spec.ts index 93672edb6c..8fabdef16b 100644 --- a/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview.spec.ts +++ b/packages/x-components/src/x-modules/queries-preview/components/__tests__/query-preview.spec.ts @@ -11,81 +11,81 @@ import { XComponentsAdapterDummy } from '../../../../__tests__/adapter.dummy'; import { getXComponentXModuleName, isXComponent } from '../../../../components/x-component.utils'; import { RootXStoreState } from '../../../../store/store.types'; import { XPlugin } from '../../../../plugins/x-plugin'; -import { QueryPreviewItem } from '../../store/types'; +import { QueryPreviewInfo, QueryPreviewItem } from '../../store/types'; import { queriesPreviewXModule } from '../../x-module'; import QueryPreview from '../query-preview.vue'; import { getEmptySearchResponseStub } from '../../../../__stubs__/index'; import { resetXQueriesPreviewStateWith } from './utils'; -describe('query preview', () => { - function renderQueryPreview({ - maxItemsToRender, - query = 'milk', - location, - queryFeature, - debounceTimeMs = 0, - template = ``, - queryPreview = { - request: { - query - }, - results: getResultsStub(4), - status: 'success', - totalResults: 100 - } - }: RenderQueryPreviewOptions = {}): RenderQueryPreviewAPI { - const localVue = createLocalVue(); - localVue.use(Vuex); - - const store = new Store>({}); - installNewXPlugin({ store }, localVue); - XPlugin.registerXModule(queriesPreviewXModule); - - const QueryPreviewRequestUpdatedSpy = jest.fn(); - XPlugin.bus.on('QueryPreviewRequestUpdated').subscribe(QueryPreviewRequestUpdatedSpy); - - if (queryPreview) { - resetXQueriesPreviewStateWith(store, { - queriesPreview: { - [query]: queryPreview - } - }); - } +function renderQueryPreview({ + maxItemsToRender, + queryPreviewInfo = { query: 'milk' }, + location, + queryFeature, + debounceTimeMs = 0, + template = ``, + queryPreview = { + request: { + query: queryPreviewInfo.query + }, + results: getResultsStub(4), + status: 'success', + totalResults: 100 + } +}: RenderQueryPreviewOptions = {}): RenderQueryPreviewAPI { + const localVue = createLocalVue(); + localVue.use(Vuex); - const wrapper = mount( - { - components: { QueryPreview }, - template, - provide: { - location - } - }, - { - localVue, - store, - propsData: { - maxItemsToRender, - query, - queryFeature, - debounceTimeMs - } + const store = new Store>({}); + installNewXPlugin({ store }, localVue); + XPlugin.registerXModule(queriesPreviewXModule); + + const queryPreviewRequestUpdatedSpy = jest.fn(); + XPlugin.bus.on('QueryPreviewRequestUpdated').subscribe(queryPreviewRequestUpdatedSpy); + + if (queryPreview) { + resetXQueriesPreviewStateWith(store, { + queriesPreview: { + [queryPreviewInfo.query]: queryPreview } - ).findComponent(QueryPreview); - - return { - wrapper, - QueryPreviewRequestUpdatedSpy, - query, - queryPreview, - findTestDataById: findTestDataById.bind(undefined, wrapper), - updateExtraParams: async params => { - store.commit('x/queriesPreview/setParams', params); - await localVue.nextTick(); - }, - reRender: () => new Promise(resolve => setTimeout(resolve)) - }; + }); } + const wrapper = mount( + { + components: { QueryPreview }, + template, + provide: { + location + } + }, + { + localVue, + store, + propsData: { + maxItemsToRender, + queryPreviewInfo, + queryFeature, + debounceTimeMs + } + } + ).findComponent(QueryPreview); + + return { + wrapper, + queryPreviewRequestUpdatedSpy, + queryPreviewInfo, + queryPreview, + findTestDataById: findTestDataById.bind(undefined, wrapper), + updateExtraParams: async params => { + store.commit('x/queriesPreview/setParams', params); + await localVue.nextTick(); + }, + reRender: () => new Promise(resolve => setTimeout(resolve)) + }; +} + +describe('query preview', () => { jest.useFakeTimers(); afterEach(() => { jest.runAllTimers(); @@ -102,14 +102,18 @@ describe('query preview', () => { }); it('sends the `QueryPreviewRequestUpdated` event', async () => { - const { QueryPreviewRequestUpdatedSpy, wrapper, updateExtraParams } = renderQueryPreview({}); + const { queryPreviewRequestUpdatedSpy, wrapper, updateExtraParams } = renderQueryPreview({ + queryPreviewInfo: { query: 'shoes', extraParams: { directory: 'Magrathea' } } + }); jest.advanceTimersByTime(0); // Wait for first emission. - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledWith({ - extraParams: {}, + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledWith({ + extraParams: { + directory: 'Magrathea' + }, origin: undefined, - query: 'milk', + query: 'shoes', rows: 24 }); @@ -118,33 +122,38 @@ describe('query preview', () => { // fast-forward until next timer should be executed jest.advanceTimersToNextTimer(); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(2, { - extraParams: {}, + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(2, { + extraParams: { + directory: 'Magrathea' + }, origin: 'popular_search:none', - query: 'milk', + query: 'shoes', rows: 24 }); await updateExtraParams({ store: 'Uganda' }); jest.advanceTimersToNextTimer(); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(3, { - extraParams: { store: 'Uganda' }, + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(3, { + extraParams: { + directory: 'Magrathea', + store: 'Uganda' + }, origin: 'popular_search:none', - query: 'milk', + query: 'shoes', rows: 24 }); }); it('sends the `QueryPreviewRequestUpdated` event with the correct location provided', () => { - const { QueryPreviewRequestUpdatedSpy } = renderQueryPreview({ + const { queryPreviewRequestUpdatedSpy } = renderQueryPreview({ location: 'predictive_layer', - query: 'shoes', + queryPreviewInfo: { query: 'shoes' }, queryFeature: 'query_suggestion' }); jest.advanceTimersToNextTimer(); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { extraParams: {}, origin: 'query_suggestion:predictive_layer', query: 'shoes', @@ -174,10 +183,10 @@ describe('query preview', () => { it('exposes the query, the results and the totalResults in the default slot', () => { const template = ` + :queryPreviewInfo="$attrs.queryPreviewInfo" + #default="{ results, queryPreviewInfo, totalResults}">
- {{ query }} + {{ queryPreviewInfo.query }} {{ totalResults }}
{{result.name}} @@ -185,12 +194,12 @@ describe('query preview', () => {
`; - const { query, wrapper, queryPreview, findTestDataById } = renderQueryPreview({ + const { queryPreviewInfo, wrapper, queryPreview, findTestDataById } = renderQueryPreview({ template }); expect(wrapper.find(getDataTestSelector('query-preview-query')).element).toHaveTextContent( - query + queryPreviewInfo.query ); expect(wrapper.find(getDataTestSelector('total-results')).element).toHaveTextContent( queryPreview!.totalResults.toString() @@ -205,7 +214,7 @@ describe('query preview', () => { it('allows changing the result content', () => { const template = ` - + {{result.id}} - {{result.name}} `; @@ -236,9 +245,7 @@ describe('query preview', () => { it('emits load event on success', async () => { jest.useRealTimers(); - const { wrapper, reRender } = renderQueryPreview({ - query: 'milk' - }); + const { wrapper, reRender } = renderQueryPreview(); (XComponentsAdapterDummy.search as jest.Mock).mockResolvedValueOnce({ ...getEmptySearchResponseStub(), @@ -258,9 +265,7 @@ describe('query preview', () => { it('emits error event on success if results are empty', async () => { jest.useRealTimers(); - const { wrapper, reRender } = renderQueryPreview({ - query: 'milk' - }); + const { wrapper, reRender } = renderQueryPreview(); // The status will be success (XComponentsAdapterDummy.search as jest.Mock).mockResolvedValueOnce({ @@ -281,9 +286,7 @@ describe('query preview', () => { it('emits error event on error', async () => { jest.useRealTimers(); - const { wrapper, reRender } = renderQueryPreview({ - query: 'milk' - }); + const { wrapper, reRender } = renderQueryPreview(); (XComponentsAdapterDummy.search as jest.Mock).mockRejectedValueOnce('Some error'); @@ -298,14 +301,14 @@ describe('query preview', () => { describe('debounce', () => { it('requests immediately when debounce is set to 0', () => { - const { QueryPreviewRequestUpdatedSpy } = renderQueryPreview({ + const { queryPreviewRequestUpdatedSpy } = renderQueryPreview({ debounceTimeMs: 0, - query: 'bull' + queryPreviewInfo: { query: 'bull' } }); jest.advanceTimersByTime(0); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { extraParams: {}, query: 'bull', rows: 24 @@ -313,39 +316,39 @@ describe('query preview', () => { }); it('does not emit subsequent requests that happen in less than the debounce time', async () => { - const { wrapper, QueryPreviewRequestUpdatedSpy } = renderQueryPreview({ + const { wrapper, queryPreviewRequestUpdatedSpy } = renderQueryPreview({ debounceTimeMs: 250, - query: 'bull' + queryPreviewInfo: { query: 'bull' } }); jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(1); // 250ms since mounting the component, the debounce tested - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(1, { extraParams: {}, query: 'bull', rows: 24 }); jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); // Emulates user is typing a new query - await wrapper.setProps({ query: 'secall' }); // Timer relaunched + await wrapper.setProps({ queryPreviewInfo: { query: 'secall' } }); // Timer relaunched jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); - await wrapper.setProps({ query: 'secallona' }); // Timer relaunched + await wrapper.setProps({ queryPreviewInfo: { query: 'secallona' } }); // Timer relaunched jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(1); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(2); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(2, { + jest.advanceTimersByTime(251); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(2); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenNthCalledWith(2, { extraParams: {}, query: 'secallona', rows: 24 @@ -354,34 +357,34 @@ describe('query preview', () => { // eslint-disable-next-line max-len it('updates the debounced request reactively when the debounceTimeMs prop changes', async () => { - const { wrapper, QueryPreviewRequestUpdatedSpy } = renderQueryPreview({ + const { wrapper, queryPreviewRequestUpdatedSpy } = renderQueryPreview({ debounceTimeMs: 250, - query: 'bull' + queryPreviewInfo: { query: 'bull' } }); jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); // Updating the debounce time aborts previous running timers await wrapper.setProps({ debounceTimeMs: 100 }); jest.advanceTimersByTime(99); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(1); // 100ms since mounting the component, the debounce tested - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); }); it('cancels pending requests when the component is destroyed', () => { - const { wrapper, QueryPreviewRequestUpdatedSpy } = renderQueryPreview({ + const { wrapper, queryPreviewRequestUpdatedSpy } = renderQueryPreview({ debounceTimeMs: 250, - query: 'bull' + queryPreviewInfo: { query: 'bull' } }); jest.advanceTimersByTime(249); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); wrapper.destroy(); jest.advanceTimersByTime(1); - expect(QueryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); + expect(queryPreviewRequestUpdatedSpy).toHaveBeenCalledTimes(0); }); }); }); @@ -389,8 +392,8 @@ describe('query preview', () => { interface RenderQueryPreviewOptions { /** The maximum number of results to render. */ maxItemsToRender?: number; - /** The query for which preview its results. */ - query?: string; + /** The query preview info for which preview its results. */ + queryPreviewInfo?: QueryPreviewInfo; /** The location of the query preview in the DOM. */ location?: string; /** The name of the tool that generated the query. */ @@ -410,9 +413,9 @@ interface RenderQueryPreviewAPI { /** The Vue testing utils wrapper for the {@link QueryPreview} component. */ wrapper: Wrapper; /** A Jest spy set in the {@link XPlugin} `on` function. */ - QueryPreviewRequestUpdatedSpy?: jest.Mock; + queryPreviewRequestUpdatedSpy?: jest.Mock; /** The query for which preview its results. */ - query: string; + queryPreviewInfo: QueryPreviewInfo; /** The results preview for the passed query. */ queryPreview: QueryPreviewItem | null; /** Find test data in the wrapper for the {@link QueryPreview} component. */ diff --git a/packages/x-components/src/x-modules/queries-preview/components/index.ts b/packages/x-components/src/x-modules/queries-preview/components/index.ts index 63cc4c4874..8001b2d0fa 100644 --- a/packages/x-components/src/x-modules/queries-preview/components/index.ts +++ b/packages/x-components/src/x-modules/queries-preview/components/index.ts @@ -1,2 +1,3 @@ export { default as QueryPreview } from './query-preview.vue'; +export { default as QueryPreviewButton } from './query-preview-button.vue'; export { default as QueryPreviewList } from './query-preview-list.vue'; diff --git a/packages/x-components/src/x-modules/queries-preview/components/query-preview-button.vue b/packages/x-components/src/x-modules/queries-preview/components/query-preview-button.vue new file mode 100644 index 0000000000..cbd110be42 --- /dev/null +++ b/packages/x-components/src/x-modules/queries-preview/components/query-preview-button.vue @@ -0,0 +1,151 @@ + + + + + +## Examples + +### Basic example + +The component content has the query preview query as default + +```vue + + + +``` + +### Customizing slots + +The content of the button is customizable via its default slot + +```vue + + + +``` + +## Events + +A list of events that the component will emit: + +- `UserAcceptedAQueryPreview`: the event is emitted after the user clicks the button. The event + payload is the `QueryPreviewInfo` of the query. + diff --git a/packages/x-components/src/x-modules/queries-preview/components/query-preview-list.vue b/packages/x-components/src/x-modules/queries-preview/components/query-preview-list.vue index 47c5f72413..b97c4aa522 100644 --- a/packages/x-components/src/x-modules/queries-preview/components/query-preview-list.vue +++ b/packages/x-components/src/x-modules/queries-preview/components/query-preview-list.vue @@ -1,7 +1,16 @@