Commit 5dbe7107 authored by Natalia Tepluhina's avatar Natalia Tepluhina

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

Copy existing components to new folder

See merge request gitlab-org/gitlab!70388
parents 78838244 6dd10023
<script>
import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { sortableFields } from '~/packages/list/utils';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import PackageTypeToken from './tokens/package_type_token.vue';
export default {
tokens: [
{
type: 'type',
icon: 'package',
title: s__('PackageRegistry|Type'),
unique: true,
token: PackageTypeToken,
operators: OPERATOR_IS_ONLY,
},
],
components: { RegistrySearch, UrlSync },
computed: {
...mapState({
isGroupPage: (state) => state.config.isGroupPage,
sorting: (state) => state.sorting,
filter: (state) => state.filter,
}),
sortableFields() {
return sortableFields(this.isGroupPage);
},
},
methods: {
...mapActions(['setSorting', 'setFilter']),
updateSorting(newValue) {
this.setSorting(newValue);
this.$emit('update');
},
},
};
</script>
<template>
<url-sync>
<template #default="{ updateQuery }">
<registry-search
:filter="filter"
:sorting="sorting"
:tokens="$options.tokens"
:sortable-fields="sortableFields"
@sorting:changed="updateSorting"
@filter:changed="setFilter"
@filter:submit="$emit('update')"
@query:changed="updateQuery"
/>
</template>
</url-sync>
</template>
<script>
import { n__ } from '~/locale';
import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
export default {
name: 'PackageTitle',
components: {
TitleArea,
MetadataItem,
},
props: {
count: {
type: Number,
required: false,
default: null,
},
helpUrl: {
type: String,
required: true,
},
},
computed: {
showPackageCount() {
return Number.isInteger(this.count);
},
packageAmountText() {
return n__(`%d Package`, `%d Packages`, this.count);
},
infoMessages() {
return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
},
},
i18n: {
LIST_TITLE_TEXT,
},
};
</script>
<template>
<title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
<template #metadata-amount>
<metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" />
</template>
</title-area>
</template>
<script>
import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
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 { packageTypeToTrackCategory } from '~/packages/shared/utils';
import Tracking from '~/tracking';
export default {
components: {
GlPagination,
GlModal,
GlSprintf,
PackagesListLoader,
PackagesListRow,
},
mixins: [Tracking.mixin()],
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)
: undefined;
return {
category,
};
},
},
methods: {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
this.$refs.packageListDeleteModal.show();
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
this.track(TrackingActions.DELETE_PACKAGE);
this.itemToBeDeleted = null;
},
deleteItemCanceled() {
this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
this.itemToBeDeleted = null;
},
},
i18n: {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column">
<slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
<div v-else-if="isLoading">
<packages-list-loader />
</div>
<template v-else>
<div data-qa-selector="packages-table">
<packages-list-row
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"
/>
<gl-modal
ref="packageListDeleteModal"
modal-id="confirm-delete-pacakge"
ok-variant="danger"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
</template>
</gl-sprintf>
</gl-modal>
</template>
</div>
</template>
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import PackageList from './packages_list.vue';
export default {
components: {
GlEmptyState,
GlLink,
GlSprintf,
PackageList,
PackageTitle: () =>
import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'),
PackageSearch: () =>
import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'),
InfrastructureTitle: () =>
import(
/* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'
),
InfrastructureSearch: () =>
import(
/* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'
),
},
inject: {
titleComponent: {
from: 'titleComponent',
default: 'PackageTitle',
},
searchComponent: {
from: 'searchComponent',
default: 'PackageSearch',
},
emptyPageTitle: {
from: 'emptyPageTitle',
default: s__('PackageRegistry|There are no packages yet'),
},
noResultsText: {
from: 'noResultsText',
default: s__(
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
),
},
},
computed: {
...mapState({
emptyListIllustration: (state) => state.config.emptyListIllustration,
emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
filter: (state) => state.filter,
selectedType: (state) => state.selectedType,
packageHelpUrl: (state) => state.config.packageHelpUrl,
packagesCount: (state) => state.pagination?.total,
}),
emptySearch() {
return (
this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0
);
},
emptyStateTitle() {
return this.emptySearch
? this.emptyPageTitle
: s__('PackageRegistry|Sorry, your filter produced no results');
},
},
mounted() {
const queryParams = getQueryParams(window.document.location.search);
const { sorting, filters } = extractFilterAndSorting(queryParams);
this.setSorting(sorting);
this.setFilter(filters);
this.requestPackagesList();
this.checkDeleteAlert();
},
methods: {
...mapActions([
'requestPackagesList',
'requestDeletePackage',
'setSelectedType',
'setSorting',
'setFilter',
]),
onPageChanged(page) {
return this.requestPackagesList({ page });
},
onPackageDeleteRequest(item) {
return this.requestDeletePackage(item);
},
checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
},
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
},
};
</script>
<template>
<div>
<component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" />
<component :is="searchComponent" @update="requestPackagesList" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
<template #description>
<gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
<gl-sprintf v-else :message="noResultsText">
<template #noPackagesLink="{ content }">
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-empty-state>
</template>
</package-list>
</div>
</template>
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { PACKAGE_TYPES } from '~/packages/list/constants';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
},
PACKAGE_TYPES,
};
</script>
<template>
<gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
<template #suggestions>
<gl-filtered-search-suggestion
v-for="(type, index) in $options.PACKAGE_TYPES"
:key="index"
:value="type.type"
>
{{ type.title }}
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import PackagesListApp from '../components/list/packages_list_app.vue';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
return new Vue({
el,
render(createElement) {
return createElement(PackagesListApp);
},
});
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_list_app renders 1`] = `
<div>
<div
help-url="foo"
/>
<div />
<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="helpSvg"
/>
</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="helpUrl"
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>
`;
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import createFlash from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/packages_list_app.vue';
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('~/flash');
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_list_app', () => {
let wrapper;
let store;
const PackageList = {
name: 'package-list',
template: '<div><slot name="empty-state"></slot></div>',
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
// we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279
const PackageSearch = { name: 'PackageSearch', template: '<div></div>' };
const PackageTitle = { name: 'PackageTitle', template: '<div></div>' };
const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' };
const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' };
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
const findPackageSearch = () => wrapper.find(PackageSearch);
const findPackageTitle = () => wrapper.find(PackageTitle);
const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle);
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
const createStore = (filter = []) => {
store = new Vuex.Store({
state: {
isLoading: false,
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl,
packageHelpUrl: 'foo',
},
filter,
},
});
store.dispatch = jest.fn();
};
const mountComponent = (provide) => {
wrapper = shallowMount(PackageListApp, {
localVue,
store,
stubs: {
GlEmptyState,
GlLoadingIcon,
PackageList,
GlSprintf,
GlLink,
PackageSearch,
PackageTitle,
InfrastructureTitle,
InfrastructureSearch,
},
provide,
});
};
beforeEach(() => {
createStore();
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
});
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('call requestPackagesList on page:changed', () => {
mountComponent();
store.dispatch.mockClear();
const list = findListComponent();
list.vm.$emit('page:changed', 1);
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
});
it('call requestDeletePackage on package:delete', () => {
mountComponent();
const list = findListComponent();
list.vm.$emit('package:delete', 'foo');
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
it('does call requestPackagesList only one time on render', () => {
mountComponent();
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');
});
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('calls setFilter with the query string based filters', () => {
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
{ type: 'type', value: { data: defaultQueryParamsMock.type } },
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
]);
});
it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
jest
.spyOn(packageUtils, 'extractFilterAndSorting')
.mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
mountComponent();
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['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('includes the right content on the default tab', () => {
mountComponent();
const heading = findEmptyState().find('h1');
expect(heading.text()).toBe('There are no packages yet');
});
});
describe('filter without results', () => {
beforeEach(() => {
createStore([{ type: 'something' }]);
mountComponent();
});
it('should show specific empty message', () => {
expect(findEmptyState().text()).toContain('Sorry, your filter produced no results');
expect(findEmptyState().text()).toContain(
'To widen your search, change or remove the filters above',
);
});
});
describe('Package Search', () => {
it('exists', () => {
mountComponent();
expect(findPackageSearch().exists()).toBe(true);
});
it('on update fetches data from the store', () => {
mountComponent();
store.dispatch.mockClear();
findPackageSearch().vm.$emit('update');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
describe('Infrastructure config', () => {
it('defaults to package registry components', () => {
mountComponent();
expect(findPackageSearch().exists()).toBe(true);
expect(findPackageTitle().exists()).toBe(true);
expect(findInfrastructureTitle().exists()).toBe(false);
expect(findInfrastructureSearch().exists()).toBe(false);
});
it('mount different component based on the provided values', () => {
mountComponent({
titleComponent: 'InfrastructureTitle',
searchComponent: 'InfrastructureSearch',
});
expect(findPackageSearch().exists()).toBe(false);
expect(findPackageTitle().exists()).toBe(false);
expect(findInfrastructureTitle().exists()).toBe(true);
expect(findInfrastructureSearch().exists()).toBe(true);
});
});
describe('delete alert handling', () => {
const originalLocation = window.location.href;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => {
createStore();
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
setWindowLocation(search);
});
afterEach(() => {
setWindowLocation(originalLocation);
});
it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
type: 'notice',
});
});
it('calls historyReplaceState with a clean url', () => {
mountComponent();
expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation);
});
it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
setWindowLocation('?');
mountComponent();
expect(createFlash).not.toHaveBeenCalled();
expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
});
});
});
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 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 PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_list', () => {
let wrapper;
let store;
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
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,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
};
store = new Vuex.Store({
state,
getters: {
getList: () => packages,
},
});
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,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when is loading', () => {
beforeEach(() => {
mountComponent({
packages: [],
isLoading: true,
});
});
it('shows skeleton loader when loading', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
});
describe('when is not loading', () => {
beforeEach(() => {
mountComponent();
});
it('does not show skeleton loader when not loading', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
});
describe('layout', () => {
beforeEach(() => {
mountComponent();
});
it('contains a pagination component', () => {
const sorting = findPackageListPagination();
expect(sorting.exists()).toBe(true);
});
it('contains a modal component', () => {
const sorting = findPackageListDeleteModal();
expect(sorting.exists()).toBe(true);
});
});
describe('when the user can destroy the package', () => {
beforeEach(() => {
mountComponent();
});
it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
const item = last(wrapper.vm.list);
findPackagesListRow().vm.$emit('packageToDelete', item);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.itemToBeDeleted).toEqual(item);
expect(mockModalShow).toHaveBeenCalled();
});
});
it('deleteItemConfirmation resets itemToBeDeleted', () => {
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemConfirmation();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
});
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('deleteItemCanceled resets itemToBeDeleted', () => {
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemCanceled();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
});
});
describe('when the list is empty', () => {
beforeEach(() => {
mountComponent({
packages: [],
slots: {
'empty-state': EmptySlotStub,
},
});
});
it('show the empty slot', () => {
const emptySlot = findEmptySlot();
expect(emptySlot.exists()).toBe(true);
});
});
describe('pagination component', () => {
let pagination;
let modelEvent;
beforeEach(() => {
mountComponent();
pagination = findPackageListPagination();
// retrieve the event used by v-model, a more sturdy approach than hardcoding it
modelEvent = pagination.vm.$options.model.event;
});
it('emits page:changed events when the page changes', () => {
pagination.vm.$emit(modelEvent, 2);
expect(wrapper.emitted('page:changed')).toEqual([[2]]);
});
});
describe('tracking', () => {
let eventSpy;
let utilSpy;
const category = 'foo';
beforeEach(() => {
mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
});
it('tracking category calls packageTypeToTrackCategory', () => {
expect(wrapper.vm.tracking.category).toBe(category);
expect(utilSpy).toHaveBeenCalledWith('conan');
});
it('deleteItemConfirmation calls event', () => {
wrapper.vm.deleteItemConfirmation();
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.DELETE_PACKAGE,
expect.any(Object),
);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { sortableFields } from '~/packages/list/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Package Search', () => {
let wrapper;
let store;
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findUrlSync = () => wrapper.findComponent(UrlSync);
const createStore = (isGroupPage) => {
const state = {
config: {
isGroupPage,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
filter: [],
};
store = new Vuex.Store({
state,
});
store.dispatch = jest.fn();
};
const mountComponent = (isGroupPage = false) => {
createStore(isGroupPage);
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
UrlSync,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('has a registry search component', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(true);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: sortableFields(),
});
});
it.each`
isGroupPage | page
${false} | ${'project'}
${true} | ${'group'}
`('in a $page page binds the right props', ({ isGroupPage }) => {
mountComponent(isGroupPage);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: sortableFields(isGroupPage),
});
});
it('on sorting:changed emits update event and calls vuex setSorting', () => {
const payload = { sort: 'foo' };
mountComponent();
findRegistrySearch().vm.$emit('sorting:changed', payload);
expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
expect(wrapper.emitted('update')).toEqual([[]]);
});
it('on filter:changed calls vuex setFilter', () => {
const payload = ['foo'];
mountComponent();
findRegistrySearch().vm.$emit('filter:changed', payload);
expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
});
it('on filter:submit emits update event', () => {
mountComponent();
findRegistrySearch().vm.$emit('filter:submit');
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 { shallowMount } from '@vue/test-utils';
import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
describe('PackageTitle', () => {
let wrapper;
let store;
const findTitleArea = () => wrapper.find(TitleArea);
const findMetadataItem = () => wrapper.find(MetadataItem);
const mountComponent = (propsData = { helpUrl: 'foo' }) => {
wrapper = shallowMount(PackageTitle, {
store,
propsData,
stubs: {
TitleArea,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('title area', () => {
it('exists', () => {
mountComponent();
expect(findTitleArea().exists()).toBe(true);
});
it('has the correct props', () => {
mountComponent();
expect(findTitleArea().props()).toMatchObject({
title: LIST_TITLE_TEXT,
infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
});
});
});
describe.each`
count | exist | text
${null} | ${false} | ${''}
${undefined} | ${false} | ${''}
${0} | ${true} | ${'0 Packages'}
${1} | ${true} | ${'1 Package'}
${2} | ${true} | ${'2 Packages'}
`('when count is $count metadata item', ({ count, exist, text }) => {
beforeEach(() => {
mountComponent({ count, helpUrl: 'foo' });
});
it(`is ${exist} that it exists`, () => {
expect(findMetadataItem().exists()).toBe(exist);
});
if (exist) {
it('has the correct props', () => {
expect(findMetadataItem().props()).toMatchObject({
icon: 'package',
text,
});
});
}
});
});
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import component from '~/packages/list/components/tokens/package_type_token.vue';
import { PACKAGE_TYPES } from '~/packages/list/constants';
describe('packages_filter', () => {
let wrapper;
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const mountComponent = ({ attrs, listeners } = {}) => {
wrapper = shallowMount(component, {
attrs,
listeners,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('it binds all of his attrs to filtered search token', () => {
mountComponent({ attrs: { foo: 'bar' } });
expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
});
it('it binds all of his events to filtered search token', () => {
const clickListener = jest.fn();
mountComponent({ listeners: { click: clickListener } });
findFilteredSearchToken().vm.$emit('click');
expect(clickListener).toHaveBeenCalled();
});
it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
'displays a suggestion for %p',
(packageType, index) => {
mountComponent();
const item = findFilteredSearchSuggestions().at(index);
expect(item.text()).toBe(packageType.title);
expect(item.props('value')).toBe(packageType.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