Commit 50e1ace0 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '330846-convert-package-list-page-to-use-apollo-graphql' into 'master'

Refactor and connect package list for GraphQl implementation

See merge request gitlab-org/gitlab!72225
parents 9006f8ed a797d196
......@@ -4,7 +4,7 @@
* For a complete overview of the plan please check: https://gitlab.com/gitlab-org/gitlab/-/issues/330846
* This work is behind feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/341136
*/
// import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
......@@ -15,17 +15,18 @@ import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import PackageTitle from './package_title.vue';
import PackageSearch from './package_search.vue';
// import PackageList from './packages_list.vue';
import PackageList from './packages_list.vue';
export default {
components: {
// GlEmptyState,
// GlLink,
// GlSprintf,
// PackageList,
GlEmptyState,
GlLink,
GlSprintf,
PackageList,
PackageTitle,
PackageSearch,
},
......@@ -64,17 +65,24 @@ export default {
groupSort: this.isGroupPage ? this.sort : undefined,
packageName: this.filters?.packageName,
packageType: this.filters?.packageType,
first: GRAPHQL_PAGE_SIZE,
};
},
graphqlResource() {
return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE;
},
pageInfo() {
return this.packages?.pageInfo ?? {};
},
packagesCount() {
return this.packages?.count;
},
hasFilters() {
return this.filters.packageName && this.filters.packageType;
},
emptySearch() {
return !this.filters.packageName && !this.filters.packageType;
},
emptyStateTitle() {
return this.emptySearch
? this.$options.i18n.emptyPageTitle
......@@ -99,6 +107,35 @@ export default {
this.sort = sort;
this.filters = { ...filters };
},
updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
fetchNextPage() {
const variables = {
...this.queryVariables,
first: GRAPHQL_PAGE_SIZE,
last: null,
after: this.pageInfo?.endCursor,
};
this.$apollo.queries.packages.fetchMore({
variables,
updateQuery: this.updateQuery,
});
},
fetchPreviousPage() {
const variables = {
...this.queryVariables,
first: null,
last: GRAPHQL_PAGE_SIZE,
before: this.pageInfo?.startCursor,
};
this.$apollo.queries.packages.fetchMore({
variables,
updateQuery: this.updateQuery,
});
},
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
......@@ -116,7 +153,13 @@ export default {
<package-title :help-url="packageHelpUrl" :count="packagesCount" />
<package-search @update="handleSearchUpdate" />
<!-- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<package-list
:list="packages.nodes"
:is-loading="$apollo.queries.packages.loading"
:page-info="pageInfo"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
>
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
<template #description>
......@@ -129,6 +172,6 @@ export default {
</template>
</gl-empty-state>
</template>
</package-list> -->
</package-list>
</div>
</template>
<script>
import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
import { s__ } from '~/locale';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { TrackingActions } from '~/packages/shared/constants';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
import Tracking from '~/tracking';
export default {
components: {
GlPagination,
GlKeysetPagination,
GlModal,
GlSprintf,
PackagesListLoader,
PackagesListRow,
},
mixins: [Tracking.mixin()],
props: {
list: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
pageInfo: {
type: Object,
required: true,
},
},
data() {
return {
itemToBeDeleted: null,
};
},
computed: {
...mapState({
perPage: (state) => state.pagination.perPage,
totalItems: (state) => state.pagination.total,
page: (state) => state.pagination.page,
isGroupPage: (state) => state.config.isGroupPage,
isLoading: 'isLoading',
}),
...mapGetters({ list: 'getList' }),
currentPage: {
get() {
return this.page;
},
set(value) {
this.$emit('page:changed', value);
},
},
isListEmpty() {
return !this.list || this.list.length === 0;
},
modalAction() {
return s__('PackageRegistry|Delete package');
},
deletePackageName() {
return this.itemToBeDeleted?.name ?? '';
},
tracking() {
const category = this.itemToBeDeleted
? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
? packageTypeToTrackCategory(this.itemToBeDeleted.packageType)
: undefined;
return {
category,
};
},
showPagination() {
return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
},
},
methods: {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION);
this.$refs.packageListDeleteModal.show();
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
this.track(TrackingActions.DELETE_PACKAGE);
this.track(DELETE_PACKAGE_TRACKING_ACTION);
this.itemToBeDeleted = null;
},
deleteItemCanceled() {
this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION);
this.itemToBeDeleted = null;
},
},
......@@ -77,6 +81,7 @@ export default {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
modalAction: s__('PackageRegistry|Delete package'),
},
};
</script>
......@@ -95,19 +100,19 @@ export default {
v-for="packageEntity in list"
:key="packageEntity.id"
:package-entity="packageEntity"
:package-link="packageEntity._links.web_path"
:is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
</div>
<gl-pagination
v-model="currentPage"
:per-page="perPage"
:total-items="totalItems"
align="center"
class="gl-w-full gl-mt-3"
/>
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
v-bind="pageInfo"
class="gl-mt-3"
@prev="$emit('prev-page')"
@next="$emit('next-page')"
/>
</div>
<gl-modal
ref="packageListDeleteModal"
......@@ -116,8 +121,8 @@ export default {
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<template #modal-title>{{ $options.i18n.modalAction }}</template>
<template #modal-ok>{{ $options.i18n.modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
......
......@@ -59,12 +59,6 @@ export const TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND =
export const TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND =
'copy_composer_package_include_command';
export const TrackingCategories = {
[PACKAGE_TYPE_MAVEN]: 'MavenPackages',
[PACKAGE_TYPE_NPM]: 'NpmPackages',
[PACKAGE_TYPE_CONAN]: 'ConanPackages',
};
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
......@@ -93,3 +87,4 @@ export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
export const PROJECT_RESOURCE_TYPE = 'project';
export const GROUP_RESOURCE_TYPE = 'group';
export const LIST_QUERY_DEBOUNCE_TIME = 50;
export const GRAPHQL_PAGE_SIZE = 20;
#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getPackages(
$fullPath: ID!
......@@ -7,21 +8,47 @@ query getPackages(
$groupSort: PackageGroupSort
$packageName: String
$packageType: PackageTypeEnum
$first: Int
$last: Int
$after: String
$before: String
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
packages(sort: $sort, packageName: $packageName, packageType: $packageType) {
packages(
sort: $sort
packageName: $packageName
packageType: $packageType
after: $after
before: $before
first: $first
last: $last
) {
count
nodes {
...PackageData
}
pageInfo {
...PageInfo
}
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) {
packages(
sort: $groupSort
packageName: $packageName
packageType: $packageType
after: $after
before: $before
first: $first
last: $last
) {
count
nodes {
...PackageData
}
pageInfo {
...PageInfo
}
}
}
}
import { capitalize } from 'lodash';
import { s__ } from '~/locale';
import {
PACKAGE_TYPE_CONAN,
......@@ -38,3 +39,5 @@ export const getPackageTypeLabel = (packageType) => {
return null;
}
};
export const packageTypeToTrackCategory = (type) => `UI::${capitalize(type)}Packages`;
......@@ -8,5 +8,62 @@ exports[`PackagesListApp renders 1`] = `
/>
<package-search-stub />
<div>
<section
class="row empty-state text-center"
>
<div
class="col-12"
>
<div
class="svg-250 svg-content"
>
<img
alt=""
class="gl-max-w-full"
role="img"
src="emptyListIllustration"
/>
</div>
</div>
<div
class="col-12"
>
<div
class="text-content gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="h4"
>
There are no packages yet
</h1>
<p>
Learn how to
<b-link-stub
class="gl-link"
event="click"
href="emptyListHelpUrl"
routertag="a"
target="_blank"
>
publish and share your packages
</b-link-stub>
with GitLab.
</p>
<div
class="gl-display-flex gl-flex-wrap gl-justify-content-center"
>
<!---->
<!---->
</div>
</div>
</div>
</section>
</div>
</div>
`;
......@@ -2,22 +2,25 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import { packagesListQuery } from '../../mock_data';
import { packagesListQuery, packageData, pagination } from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
......@@ -39,11 +42,19 @@ describe('PackagesListApp', () => {
const PackageList = {
name: 'package-list',
template: '<div><slot name="empty-state"></slot></div>',
props: OriginalPackageList.props,
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const searchPayload = {
sort: 'VERSION_DESC',
filters: { packageName: 'foo', packageType: 'CONAN' },
};
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
......@@ -105,25 +116,55 @@ describe('PackagesListApp', () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
const payload = {
sort: 'VERSION_DESC',
filters: { packageName: 'foo', packageType: 'CONAN' },
};
findSearch().vm.$emit('update', payload);
findSearch().vm.$emit('update', searchPayload);
await waitForDebouncedApollo();
jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
groupSort: payload.sort,
...payload.filters,
groupSort: searchPayload.sort,
...searchPayload.filters,
}),
);
});
});
describe('list component', () => {
let resolver;
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
return waitForDebouncedApollo();
});
it('exists and has the right props', () => {
expect(findListComponent().props()).toMatchObject({
list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
isLoading: false,
pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
});
});
it('when list emits next-page fetches the next set of records', () => {
findListComponent().vm.$emit('next-page');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
);
});
it('when list emits prev-page fetches the prev set of records', () => {
findListComponent().vm.$emit('prev-page');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
);
});
});
describe.each`
type | sortType
${PROJECT_RESOURCE_TYPE} | ${'sort'}
......@@ -136,7 +177,7 @@ describe('PackagesListApp', () => {
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
resolver = jest.fn().mockResolvedValue(packagesListQuery({ type }));
mountComponent({ provide, resolver });
return waitForDebouncedApollo();
});
......@@ -151,4 +192,40 @@ describe('PackagesListApp', () => {
);
});
});
describe('empty state', () => {
beforeEach(() => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
mountComponent({ resolver });
return waitForDebouncedApollo();
});
it('generate the correct empty list link', () => {
const link = findListComponent().findComponent(GlLink);
expect(link.attributes('href')).toBe(defaultProvide.emptyListHelpUrl);
expect(link.text()).toBe('publish and share your packages');
});
it('includes the right content on the default tab', () => {
expect(findEmptyState().text()).toContain(PackageListApp.i18n.emptyPageTitle);
});
});
describe('filter without results', () => {
beforeEach(async () => {
mountComponent();
await waitForDebouncedApollo();
findSearch().vm.$emit('update', searchPayload);
return nextTick();
});
it('should show specific empty message', () => {
expect(findEmptyState().text()).toContain(PackageListApp.i18n.noResultsTitle);
expect(findEmptyState().text()).toContain(PackageListApp.i18n.widenFilters);
});
});
});
import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import { packageList } from 'jest/packages/mock_data';
import { GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { TrackingActions } from '~/packages/shared/constants';
import * as SharedUtils from '~/packages/shared/utils';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
const localVue = createLocalVue();
localVue.use(Vuex);
import { packageData } from '../../mock_data';
describe('packages_list', () => {
let wrapper;
let store;
const firstPackage = packageData();
const secondPackage = {
...packageData(),
id: 'gid://gitlab/Packages::Package/112',
name: 'second-package',
};
const defaultProps = {
list: [firstPackage, secondPackage],
isLoading: false,
pageInfo: {},
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const GlModalStub = {
name: GlModal.name,
template: '<div><slot></slot></div>',
methods: { show: jest.fn() },
};
const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
const findPackageListPagination = () => wrapper.find(GlPagination);
const findPackageListDeleteModal = () => wrapper.find(GlModal);
const findEmptySlot = () => wrapper.find(EmptySlotStub);
const findPackagesListRow = () => wrapper.find(PackagesListRow);
const createStore = (isGroupPage, packages, isLoading) => {
const state = {
isLoading,
packages,
pagination: {
perPage: 1,
total: 1,
page: 1,
},
config: {
isGroupPage,
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination);
const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
propsData: {
...defaultProps,
...props,
},
sorting: {
orderBy: 'version',
sort: 'desc',
stubs: {
GlModal: GlModalStub,
GlSprintf,
},
};
store = new Vuex.Store({
state,
getters: {
getList: () => packages,
slots: {
'empty-state': EmptySlotStub,
},
});
store.dispatch = jest.fn();
};
const mountComponent = ({
isGroupPage = false,
packages = packageList,
isLoading = false,
...options
} = {}) => {
createStore(isGroupPage, packages, isLoading);
wrapper = mount(PackagesList, {
localVue,
store,
stubs: {
...stubChildren(PackagesList),
GlTable,
GlModal,
},
...options,
});
};
beforeEach(() => {
GlModalStub.methods.show.mockReset();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when is loading', () => {
beforeEach(() => {
mountComponent({
packages: [],
isLoading: true,
});
mountComponent({ isLoading: true });
});
it('shows skeleton loader when loading', () => {
it('shows skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
it('does not show the rows', () => {
expect(findPackagesListRow().exists()).toBe(false);
});
it('does not show the pagination', () => {
expect(findPackageListPagination().exists()).toBe(false);
});
});
describe('when is not loading', () => {
......@@ -95,74 +88,68 @@ describe('packages_list', () => {
mountComponent();
});
it('does not show skeleton loader when not loading', () => {
it('does not show skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
});
describe('layout', () => {
beforeEach(() => {
mountComponent();
it('shows the rows', () => {
expect(findPackagesListRow().exists()).toBe(true);
});
});
describe('layout', () => {
it('contains a pagination component', () => {
const sorting = findPackageListPagination();
expect(sorting.exists()).toBe(true);
mountComponent({ pageInfo: { hasPreviousPage: true } });
expect(findPackageListPagination().exists()).toBe(true);
});
it('contains a modal component', () => {
const sorting = findPackageListDeleteModal();
expect(sorting.exists()).toBe(true);
mountComponent();
expect(findPackageListDeleteModal().exists()).toBe(true);
});
});
describe('when the user can destroy the package', () => {
beforeEach(() => {
mountComponent();
findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
return nextTick();
});
it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
const item = last(wrapper.vm.list);
it('deleting a package opens the modal', () => {
expect(findPackageListDeleteModal().text()).toContain(firstPackage.name);
});
findPackagesListRow().vm.$emit('packageToDelete', item);
it('confirming delete empties itemsToBeDeleted', async () => {
findPackageListDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.itemToBeDeleted).toEqual(item);
expect(mockModalShow).toHaveBeenCalled();
});
});
await nextTick();
it('deleteItemConfirmation resets itemToBeDeleted', () => {
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemConfirmation();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
});
it('deleteItemConfirmation emit package:delete', () => {
const itemToBeDeleted = { id: 2 };
wrapper.setData({ itemToBeDeleted });
wrapper.vm.deleteItemConfirmation();
return wrapper.vm.$nextTick(() => {
expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
});
it('confirming on the modal emits package:delete', async () => {
findPackageListDeleteModal().vm.$emit('ok');
await nextTick();
expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
it('deleteItemCanceled resets itemToBeDeleted', () => {
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemCanceled();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
it('cancel event resets itemToBeDeleted', async () => {
findPackageListDeleteModal().vm.$emit('cancel');
await nextTick();
expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
});
});
describe('when the list is empty', () => {
beforeEach(() => {
mountComponent({
packages: [],
slots: {
'empty-state': EmptySlotStub,
},
});
mountComponent({ list: [] });
});
it('show the empty slot', () => {
......@@ -171,45 +158,59 @@ describe('packages_list', () => {
});
});
describe('pagination component', () => {
let pagination;
let modelEvent;
describe('pagination ', () => {
beforeEach(() => {
mountComponent();
pagination = findPackageListPagination();
// retrieve the event used by v-model, a more sturdy approach than hardcoding it
modelEvent = pagination.vm.$options.model.event;
mountComponent({ pageInfo: { hasPreviousPage: true } });
});
it('emits prev-page events when the prev event is fired', () => {
findPackageListPagination().vm.$emit('prev');
expect(wrapper.emitted('prev-page')).toEqual([[]]);
});
it('emits page:changed events when the page changes', () => {
pagination.vm.$emit(modelEvent, 2);
expect(wrapper.emitted('page:changed')).toEqual([[2]]);
it('emits next-page events when the next event is fired', () => {
findPackageListPagination().vm.$emit('next');
expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
describe('tracking', () => {
let eventSpy;
let utilSpy;
const category = 'foo';
const category = 'UI::NpmPackages';
beforeEach(() => {
mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
mountComponent();
findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
return nextTick();
});
it('tracking category calls packageTypeToTrackCategory', () => {
expect(wrapper.vm.tracking.category).toBe(category);
expect(utilSpy).toHaveBeenCalledWith('conan');
it('requesting the delete tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
category,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
it('deleteItemConfirmation calls event', () => {
wrapper.vm.deleteItemConfirmation();
it('confirming delete tracks the right action', () => {
findPackageListDeleteModal().vm.$emit('ok');
expect(eventSpy).toHaveBeenCalledWith(
category,
DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
it('canceling delete tracks the right action', () => {
findPackageListDeleteModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.DELETE_PACKAGE,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
......
import capitalize from 'lodash/capitalize';
export const packageTags = () => [
{ id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' },
......@@ -156,6 +158,15 @@ export const nugetMetadata = () => ({
projectUrl: 'projectUrl',
});
export const pagination = (extend) => ({
endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0',
__typename: 'PageInfo',
...extend,
});
export const packageDetailsQuery = (extendPackage) => ({
data: {
package: {
......@@ -256,7 +267,7 @@ export const packageDestroyFileMutationError = () => ({
],
});
export const packagesListQuery = (type = 'group') => ({
export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({
data: {
[type]: {
packages: {
......@@ -277,9 +288,11 @@ export const packagesListQuery = (type = 'group') => ({
pipelines: { nodes: [] },
},
],
pageInfo: pagination(extendPagination),
__typename: 'PackageConnection',
},
__typename: 'Group',
...extend,
__typename: capitalize(type),
},
},
});
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