Commit ccb81a57 authored by Mark Florian's avatar Mark Florian

Merge branch '324864-make-it-easy-to-share-a-filtered-view-of-the-registry-2' into 'master'

Connect Registries searches to URL

See merge request gitlab-org/gitlab!57251
parents b0de07d0 269fbf52
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import getTableHeaders from '../utils'; import getTableHeaders from '../utils';
import PackageTypeToken from './tokens/package_type_token.vue'; import PackageTypeToken from './tokens/package_type_token.vue';
...@@ -16,7 +17,7 @@ export default { ...@@ -16,7 +17,7 @@ export default {
operators: [{ value: '=', description: __('is'), default: 'true' }], operators: [{ value: '=', description: __('is'), default: 'true' }],
}, },
], ],
components: { RegistrySearch }, components: { RegistrySearch, UrlSync },
computed: { computed: {
...mapState({ ...mapState({
isGroupPage: (state) => state.config.isGroupPage, isGroupPage: (state) => state.config.isGroupPage,
...@@ -38,6 +39,8 @@ export default { ...@@ -38,6 +39,8 @@ export default {
</script> </script>
<template> <template>
<url-sync>
<template #default="{ updateQuery }">
<registry-search <registry-search
:filter="filter" :filter="filter"
:sorting="sorting" :sorting="sorting"
...@@ -46,5 +49,8 @@ export default { ...@@ -46,5 +49,8 @@ export default {
@sorting:changed="updateSorting" @sorting:changed="updateSorting"
@filter:changed="setFilter" @filter:changed="setFilter"
@filter:submit="$emit('update')" @filter:submit="$emit('update')"
@query:changed="updateQuery"
/> />
</template>
</url-sync>
</template> </template>
...@@ -6,6 +6,7 @@ import { historyReplaceState } from '~/lib/utils/common_utils'; ...@@ -6,6 +6,7 @@ import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageSearch from './package_search.vue'; import PackageSearch from './package_search.vue';
import PackageTitle from './package_title.vue'; import PackageTitle from './package_title.vue';
...@@ -42,11 +43,21 @@ export default { ...@@ -42,11 +43,21 @@ export default {
}, },
}, },
mounted() { mounted() {
const queryParams = getQueryParams(window.document.location.search);
const { sorting, filters } = extractFilterAndSorting(queryParams);
this.setSorting(sorting);
this.setFilter(filters);
this.requestPackagesList(); this.requestPackagesList();
this.checkDeleteAlert(); this.checkDeleteAlert();
}, },
methods: { methods: {
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']), ...mapActions([
'requestPackagesList',
'requestDeletePackage',
'setSelectedType',
'setSorting',
'setFilter',
]),
onPageChanged(page) { onPageChanged(page) {
return this.requestPackagesList({ page }); return this.requestPackagesList({ page });
}, },
......
...@@ -7,3 +7,23 @@ export const keyValueToFilterToken = (type, data) => ({ type, value: { data } }) ...@@ -7,3 +7,23 @@ export const keyValueToFilterToken = (type, data) => ({ type, value: { data } })
export const searchArrayToFilterTokens = (search) => export const searchArrayToFilterTokens = (search) =>
search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s)); search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s));
export const extractFilterAndSorting = (queryObject) => {
const { type, search, sort, orderBy } = queryObject;
const filters = [];
const sorting = {};
if (type) {
filters.push(keyValueToFilterToken('type', type));
}
if (search) {
filters.push(...searchArrayToFilterTokens(search));
}
if (sort) {
sorting.sort = sort;
}
if (orderBy) {
sorting.orderBy = orderBy;
}
return { filters, sorting };
};
...@@ -12,6 +12,7 @@ import { get } from 'lodash'; ...@@ -12,6 +12,7 @@ import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import DeleteImage from '../components/delete_image.vue'; import DeleteImage from '../components/delete_image.vue';
...@@ -82,6 +83,9 @@ export default { ...@@ -82,6 +83,9 @@ export default {
searchConfig: SORT_FIELDS, searchConfig: SORT_FIELDS,
apollo: { apollo: {
baseImages: { baseImages: {
skip() {
return !this.fetchBaseQuery;
},
query: getContainerRepositoriesQuery, query: getContainerRepositoriesQuery,
variables() { variables() {
return this.queryVariables; return this.queryVariables;
...@@ -125,15 +129,19 @@ export default { ...@@ -125,15 +129,19 @@ export default {
sorting: { orderBy: 'UPDATED', sort: 'desc' }, sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null, name: null,
mutationLoading: false, mutationLoading: false,
fetchBaseQuery: false,
fetchAdditionalDetails: false, fetchAdditionalDetails: false,
}; };
}, },
computed: { computed: {
images() { images() {
if (this.baseImages) {
return this.baseImages.map((image, index) => ({ return this.baseImages.map((image, index) => ({
...image, ...image,
...get(this.additionalDetails, index, {}), ...get(this.additionalDetails, index, {}),
})); }));
}
return [];
}, },
graphqlResource() { graphqlResource() {
return this.config.isGroupPage ? 'group' : 'project'; return this.config.isGroupPage ? 'group' : 'project';
...@@ -172,8 +180,15 @@ export default { ...@@ -172,8 +180,15 @@ export default {
}, },
}, },
mounted() { mounted() {
const { sorting, filters } = extractFilterAndSorting(this.$route.query);
this.filter = [...filters];
this.name = filters[0]?.value.data;
this.sorting = { ...this.sorting, ...sorting };
// If the two graphql calls - which are not batched - resolve togheter we will have a race // If the two graphql calls - which are not batched - resolve togheter we will have a race
// condition when apollo sets the cache, with this we give the 'base' call an headstart // condition when apollo sets the cache, with this we give the 'base' call an headstart
this.fetchBaseQuery = true;
setTimeout(() => { setTimeout(() => {
this.fetchAdditionalDetails = true; this.fetchAdditionalDetails = true;
}, 200); }, 200);
...@@ -245,6 +260,9 @@ export default { ...@@ -245,6 +260,9 @@ export default {
const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM); const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM);
this.name = search?.value?.data; this.name = search?.value?.data;
}, },
updateUrlQueryString(query) {
this.$router.push({ query });
},
}, },
}; };
</script> </script>
...@@ -304,6 +322,7 @@ export default { ...@@ -304,6 +322,7 @@ export default {
@sorting:changed="updateSorting" @sorting:changed="updateSorting"
@filter:changed="filter = $event" @filter:changed="filter = $event"
@filter:submit="doFilter" @filter:submit="doFilter"
@query:changed="updateUrlQueryString"
/> />
<div v-if="isLoading" class="gl-mt-5"> <div v-if="isLoading" class="gl-mt-5">
......
---
title: Connect Registries searches to URL
merge_request: 57251
author:
type: added
...@@ -7,6 +7,8 @@ import PackageSearch from '~/packages/list/components/package_search.vue'; ...@@ -7,6 +7,8 @@ import PackageSearch from '~/packages/list/components/package_search.vue';
import PackageListApp from '~/packages/list/components/packages_list_app.vue'; import PackageListApp from '~/packages/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import * as packageUtils from '~/packages_and_registries/shared/utils';
jest.mock('~/lib/utils/common_utils'); jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -61,6 +63,7 @@ describe('packages_list_app', () => { ...@@ -61,6 +63,7 @@ describe('packages_list_app', () => {
beforeEach(() => { beforeEach(() => {
createStore(); createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
}); });
afterEach(() => { afterEach(() => {
...@@ -72,46 +75,92 @@ describe('packages_list_app', () => { ...@@ -72,46 +75,92 @@ describe('packages_list_app', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('empty state', () => { it('call requestPackagesList on page:changed', () => {
it('generate the correct empty list link', () => {
mountComponent(); mountComponent();
store.dispatch.mockClear();
const link = findListComponent().find(GlLink); const list = findListComponent();
list.vm.$emit('page:changed', 1);
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
});
expect(link.attributes('href')).toBe(emptyListHelpUrl); it('call requestDeletePackage on package:delete', () => {
expect(link.text()).toBe('publish and share your packages'); mountComponent();
const list = findListComponent();
list.vm.$emit('package:delete', 'foo');
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
}); });
it('includes the right content on the default tab', () => { it('does call requestPackagesList only one time on render', () => {
mountComponent(); mountComponent();
const heading = findEmptyState().find('h1'); expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList');
});
expect(heading.text()).toBe('There are no packages yet'); describe('url query string handling', () => {
const defaultQueryParamsMock = {
search: [1, 2],
type: 'npm',
sort: 'asc',
orderBy: 'created',
};
it('calls setSorting with the query string based sorting', () => {
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
orderBy: defaultQueryParamsMock.orderBy,
sort: defaultQueryParamsMock.sort,
}); });
}); });
it('call requestPackagesList on page:changed', () => { it('calls setFilter with the query string based filters', () => {
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
mountComponent(); mountComponent();
store.dispatch.mockClear();
const list = findListComponent(); expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
list.vm.$emit('page:changed', 1); { type: 'type', value: { data: defaultQueryParamsMock.type } },
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 }); { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
]);
}); });
it('call requestDeletePackage on package:delete', () => { it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
jest
.spyOn(packageUtils, 'extractFilterAndSorting')
.mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
mountComponent(); mountComponent();
const list = findListComponent(); expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
list.vm.$emit('package:delete', 'foo'); expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']);
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo'); });
});
describe('empty state', () => {
it('generate the correct empty list link', () => {
mountComponent();
const link = findListComponent().find(GlLink);
expect(link.attributes('href')).toBe(emptyListHelpUrl);
expect(link.text()).toBe('publish and share your packages');
}); });
it('does not call requestPackagesList two times on render', () => { it('includes the right content on the default tab', () => {
mountComponent(); mountComponent();
expect(store.dispatch).toHaveBeenCalledTimes(1); const heading = findEmptyState().find('h1');
expect(heading.text()).toBe('There are no packages yet');
});
}); });
describe('filter without results', () => { describe('filter without results', () => {
......
...@@ -4,6 +4,7 @@ import component from '~/packages/list/components/package_search.vue'; ...@@ -4,6 +4,7 @@ import component from '~/packages/list/components/package_search.vue';
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue'; import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
import getTableHeaders from '~/packages/list/utils'; import getTableHeaders from '~/packages/list/utils';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -12,7 +13,8 @@ describe('Package Search', () => { ...@@ -12,7 +13,8 @@ describe('Package Search', () => {
let wrapper; let wrapper;
let store; let store;
const findRegistrySearch = () => wrapper.find(RegistrySearch); const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findUrlSync = () => wrapper.findComponent(UrlSync);
const createStore = (isGroupPage) => { const createStore = (isGroupPage) => {
const state = { const state = {
...@@ -37,6 +39,9 @@ describe('Package Search', () => { ...@@ -37,6 +39,9 @@ describe('Package Search', () => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
localVue, localVue,
store, store,
stubs: {
UrlSync,
},
}); });
}; };
...@@ -104,4 +109,20 @@ describe('Package Search', () => { ...@@ -104,4 +109,20 @@ describe('Package Search', () => {
expect(wrapper.emitted('update')).toEqual([[]]); expect(wrapper.emitted('update')).toEqual([[]]);
}); });
it('has a UrlSync component', () => {
mountComponent();
expect(findUrlSync().exists()).toBe(true);
});
it('on query:changed calls updateQuery from UrlSync', () => {
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
mountComponent();
findRegistrySearch().vm.$emit('query:changed');
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
});
}); });
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { import {
getQueryParams, getQueryParams,
keyValueToFilterToken, keyValueToFilterToken,
searchArrayToFilterTokens, searchArrayToFilterTokens,
extractFilterAndSorting,
} from '~/packages_and_registries/shared/utils'; } from '~/packages_and_registries/shared/utils';
describe('Packages And Registries shared utils', () => { describe('Packages And Registries shared utils', () => {
...@@ -27,9 +29,31 @@ describe('Packages And Registries shared utils', () => { ...@@ -27,9 +29,31 @@ describe('Packages And Registries shared utils', () => {
const search = ['one', 'two']; const search = ['one', 'two'];
expect(searchArrayToFilterTokens(search)).toStrictEqual([ expect(searchArrayToFilterTokens(search)).toStrictEqual([
{ type: 'filtered-search-term', value: { data: 'one' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'one' } },
{ type: 'filtered-search-term', value: { data: 'two' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'two' } },
]); ]);
}); });
}); });
describe('extractFilterAndSorting', () => {
it.each`
search | type | sort | orderBy | result
${['one']} | ${'myType'} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: 'type', value: { data: 'myType' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
${['one']} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
${[]} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
${null} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
${null} | ${null} | ${null} | ${'foo'} | ${{ sorting: { orderBy: 'foo' }, filters: [] }}
${null} | ${null} | ${null} | ${null} | ${{ sorting: {}, filters: [] }}
`(
'returns sorting and filters objects in the correct form',
({ search, type, sort, orderBy, result }) => {
const queryObject = {
search,
type,
sort,
orderBy,
};
expect(extractFilterAndSorting(queryObject)).toStrictEqual(result);
},
);
});
}); });
import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -61,7 +62,7 @@ describe('List Page', () => { ...@@ -61,7 +62,7 @@ describe('List Page', () => {
const waitForApolloRequestRender = async () => { const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
await waitForPromises(); await waitForPromises();
await wrapper.vm.$nextTick(); await nextTick();
}; };
const mountComponent = ({ const mountComponent = ({
...@@ -70,6 +71,7 @@ describe('List Page', () => { ...@@ -70,6 +71,7 @@ describe('List Page', () => {
detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock), detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock), mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
config = { isGroupPage: false }, config = { isGroupPage: false },
query = {},
} = {}) => { } = {}) => {
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -96,6 +98,7 @@ describe('List Page', () => { ...@@ -96,6 +98,7 @@ describe('List Page', () => {
$toast, $toast,
$route: { $route: {
name: 'foo', name: 'foo',
query,
}, },
...mocks, ...mocks,
}, },
...@@ -159,9 +162,11 @@ describe('List Page', () => { ...@@ -159,9 +162,11 @@ describe('List Page', () => {
}); });
describe('isLoading is true', () => { describe('isLoading is true', () => {
it('shows the skeleton loader', () => { it('shows the skeleton loader', async () => {
mountComponent(); mountComponent();
await nextTick();
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
}); });
...@@ -177,9 +182,11 @@ describe('List Page', () => { ...@@ -177,9 +182,11 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('title has the metadataLoading props set to true', () => { it('title has the metadataLoading props set to true', async () => {
mountComponent(); mountComponent();
await nextTick();
expect(findRegistryHeader().props('metadataLoading')).toBe(true); expect(findRegistryHeader().props('metadataLoading')).toBe(true);
}); });
}); });
...@@ -312,7 +319,7 @@ describe('List Page', () => { ...@@ -312,7 +319,7 @@ describe('List Page', () => {
await selectImageForDeletion(); await selectImageForDeletion();
findDeleteImage().vm.$emit('success'); findDeleteImage().vm.$emit('success');
await wrapper.vm.$nextTick(); await nextTick();
const alert = findDeleteAlert(); const alert = findDeleteAlert();
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
...@@ -328,7 +335,7 @@ describe('List Page', () => { ...@@ -328,7 +335,7 @@ describe('List Page', () => {
await selectImageForDeletion(); await selectImageForDeletion();
findDeleteImage().vm.$emit('error'); findDeleteImage().vm.$emit('error');
await wrapper.vm.$nextTick(); await nextTick();
const alert = findDeleteAlert(); const alert = findDeleteAlert();
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
...@@ -349,7 +356,7 @@ describe('List Page', () => { ...@@ -349,7 +356,7 @@ describe('List Page', () => {
findRegistrySearch().vm.$emit('filter:submit'); findRegistrySearch().vm.$emit('filter:submit');
await wrapper.vm.$nextTick(); await nextTick();
}; };
it('has a search box element', async () => { it('has a search box element', async () => {
...@@ -374,7 +381,7 @@ describe('List Page', () => { ...@@ -374,7 +381,7 @@ describe('List Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' }); findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
await wrapper.vm.$nextTick(); await nextTick();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' })); expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
}); });
...@@ -417,7 +424,7 @@ describe('List Page', () => { ...@@ -417,7 +424,7 @@ describe('List Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('prev-page'); findImageList().vm.$emit('prev-page');
await wrapper.vm.$nextTick(); await nextTick();
expect(resolver).toHaveBeenCalledWith( expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ before: pageInfo.startCursor }), expect.objectContaining({ before: pageInfo.startCursor }),
...@@ -437,7 +444,7 @@ describe('List Page', () => { ...@@ -437,7 +444,7 @@ describe('List Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('next-page'); findImageList().vm.$emit('next-page');
await wrapper.vm.$nextTick(); await nextTick();
expect(resolver).toHaveBeenCalledWith( expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pageInfo.endCursor }), expect.objectContaining({ after: pageInfo.endCursor }),
...@@ -458,13 +465,12 @@ describe('List Page', () => { ...@@ -458,13 +465,12 @@ describe('List Page', () => {
expect(findDeleteModal().exists()).toBe(true); expect(findDeleteModal().exists()).toBe(true);
}); });
it('contains a description with the path of the item to delete', () => { it('contains a description with the path of the item to delete', async () => {
findImageList().vm.$emit('delete', { path: 'foo' }); findImageList().vm.$emit('delete', { path: 'foo' });
return wrapper.vm.$nextTick().then(() => { await nextTick();
expect(findDeleteModal().html()).toContain('foo'); expect(findDeleteModal().html()).toContain('foo');
}); });
}); });
});
describe('tracking', () => { describe('tracking', () => {
beforeEach(() => { beforeEach(() => {
...@@ -498,4 +504,60 @@ describe('List Page', () => { ...@@ -498,4 +504,60 @@ describe('List Page', () => {
testTrackingCall('confirm_delete'); testTrackingCall('confirm_delete');
}); });
}); });
describe('url query string handling', () => {
const defaultQueryParams = {
search: [1, 2],
sort: 'asc',
orderBy: 'CREATED',
};
const queryChangePayload = 'foo';
it('query:updated event pushes the new query to the router', async () => {
const push = jest.fn();
mountComponent({ mocks: { $router: { push } } });
await nextTick();
findRegistrySearch().vm.$emit('query:changed', queryChangePayload);
expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
});
it('graphql API call has the variables set from the URL', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ query: defaultQueryParams, resolver });
await nextTick();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
name: 1,
sort: 'CREATED_ASC',
}),
);
});
it.each`
sort | orderBy | search | payload
${'ASC'} | ${undefined} | ${undefined} | ${{ sort: 'UPDATED_ASC' }}
${undefined} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_DESC' }}
${'ASC'} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_ASC' }}
${undefined} | ${undefined} | ${undefined} | ${{}}
${undefined} | ${undefined} | ${['one']} | ${{ name: 'one' }}
${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
${'ASC'} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
`(
'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
async ({ sort, orderBy, search, payload }) => {
const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
mountComponent({ query: { sort, orderBy, search }, resolver });
await nextTick();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
},
);
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment