Commit 5495c584 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Martin Wortschack

Update constants entries

- explicit sort
- event tracking
parent b5176790
<script> <script>
import { mapState } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { GlTable, GlPagination, GlButton, GlSorting, GlSortingItem, GlModal } from '@gitlab/ui'; import {
GlTable,
GlPagination,
GlButton,
GlSorting,
GlSortingItem,
GlModal,
GlLink,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { import {
LIST_KEY_NAME, LIST_KEY_NAME,
LIST_KEY_PROJECT, LIST_KEY_PROJECT,
...@@ -18,6 +27,9 @@ import { ...@@ -18,6 +27,9 @@ import {
LIST_LABEL_PACKAGE_TYPE, LIST_LABEL_PACKAGE_TYPE,
LIST_LABEL_CREATED_AT, LIST_LABEL_CREATED_AT,
LIST_LABEL_ACTIONS, LIST_LABEL_ACTIONS,
LIST_ORDER_BY_PACKAGE_TYPE,
ASCENDING_ODER,
DESCENDING_ORDER,
} from '../constants'; } from '../constants';
import { TrackingActions } from '../../shared/constants'; import { TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils'; import { packageTypeToTrackCategory } from '../../shared/utils';
...@@ -30,26 +42,29 @@ export default { ...@@ -30,26 +42,29 @@ export default {
GlSorting, GlSorting,
GlSortingItem, GlSortingItem,
GlButton, GlButton,
GlLink,
TimeAgoTooltip, TimeAgoTooltip,
GlModal, GlModal,
Icon, GlIcon,
PackageTags, PackageTags,
}, },
directives: { GlTooltip: GlTooltipDirective },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
data() { data() {
return { return {
modalId: 'confirm-delete-pacakge',
itemToBeDeleted: null, itemToBeDeleted: null,
}; };
}, },
computed: { computed: {
...mapState({ ...mapState({
list: 'packages',
perPage: state => state.pagination.perPage, perPage: state => state.pagination.perPage,
totalItems: state => state.pagination.total, totalItems: state => state.pagination.total,
page: state => state.pagination.page, page: state => state.pagination.page,
isGroupPage: state => state.config.isGroupPage, isGroupPage: state => state.config.isGroupPage,
orderBy: state => state.sorting.orderBy,
sort: state => state.sorting.sort,
}), }),
...mapGetters({ list: 'getList' }),
currentPage: { currentPage: {
get() { get() {
return this.page; return this.page;
...@@ -58,19 +73,12 @@ export default { ...@@ -58,19 +73,12 @@ export default {
this.$emit('page:changed', value); this.$emit('page:changed', value);
}, },
}, },
orderBy() {
return 'name';
},
sort() {
return 'asc';
},
// end of vuex placeholder
sortText() { sortText() {
const field = this.sortableFields.find(s => s.key === this.orderBy); const field = this.sortableFields.find(s => s.orderBy === this.orderBy);
return field ? field.label : ''; return field ? field.label : '';
}, },
isSortAscending() { isSortAscending() {
return this.sort === 'asc'; return this.sort === ASCENDING_ODER;
}, },
isListEmpty() { isListEmpty() {
return !this.list || this.list.length === 0; return !this.list || this.list.length === 0;
...@@ -84,26 +92,31 @@ export default { ...@@ -84,26 +92,31 @@ export default {
{ {
key: LIST_KEY_NAME, key: LIST_KEY_NAME,
label: LIST_LABEL_NAME, label: LIST_LABEL_NAME,
orderBy: LIST_KEY_NAME,
class: ['text-left'], class: ['text-left'],
}, },
{ {
key: LIST_KEY_PROJECT, key: LIST_KEY_PROJECT,
label: LIST_LABEL_PROJECT, label: LIST_LABEL_PROJECT,
orderBy: LIST_KEY_PROJECT,
class: ['text-center'], class: ['text-center'],
}, },
{ {
key: LIST_KEY_VERSION, key: LIST_KEY_VERSION,
label: LIST_LABEL_VERSION, label: LIST_LABEL_VERSION,
orderBy: LIST_KEY_VERSION,
class: ['text-center'], class: ['text-center'],
}, },
{ {
key: LIST_KEY_PACKAGE_TYPE, key: LIST_KEY_PACKAGE_TYPE,
label: LIST_LABEL_PACKAGE_TYPE, label: LIST_LABEL_PACKAGE_TYPE,
orderBy: LIST_ORDER_BY_PACKAGE_TYPE,
class: ['text-center'], class: ['text-center'],
}, },
{ {
key: LIST_KEY_CREATED_AT, key: LIST_KEY_CREATED_AT,
label: LIST_LABEL_CREATED_AT, label: LIST_LABEL_CREATED_AT,
orderBy: LIST_KEY_CREATED_AT,
class: this.showActions ? ['text-center'] : ['text-right'], class: this.showActions ? ['text-center'] : ['text-right'],
}, },
].filter(f => f.key !== LIST_KEY_PROJECT || this.isGroupPage); ].filter(f => f.key !== LIST_KEY_PROJECT || this.isGroupPage);
...@@ -141,14 +154,19 @@ export default { ...@@ -141,14 +154,19 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['setSorting']),
onDirectionChange() { onDirectionChange() {
// to be connected to the sorting api when the api is ready const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
this.setSorting({ sort });
this.$emit('sort:changed');
}, },
onSortItemClick() { onSortItemClick(item) {
// to be connected to the sorting api when the api is ready this.setSorting({ orderBy: item });
this.$emit('sort:changed');
}, },
setItemToBeDeleted(item) { setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item }; this.itemToBeDeleted = { ...item };
this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
this.$refs.packageListDeleteModal.show(); this.$refs.packageListDeleteModal.show();
}, },
deleteItemConfirmation() { deleteItemConfirmation() {
...@@ -157,7 +175,7 @@ export default { ...@@ -157,7 +175,7 @@ export default {
this.itemToBeDeleted = null; this.itemToBeDeleted = null;
}, },
deleteItemCanceled() { deleteItemCanceled() {
// this is going to be used to support ui tracking in the future this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
this.itemToBeDeleted = null; this.itemToBeDeleted = null;
}, },
}, },
...@@ -169,7 +187,6 @@ export default { ...@@ -169,7 +187,6 @@ export default {
<slot v-if="isListEmpty" name="empty-state"></slot> <slot v-if="isListEmpty" name="empty-state"></slot>
<template v-else> <template v-else>
<gl-sorting <gl-sorting
ref="packageListSorting"
class="my-3" class="my-3"
:text="sortText" :text="sortText"
:is-ascending="isSortAscending" :is-ascending="isSortAscending"
...@@ -179,39 +196,46 @@ export default { ...@@ -179,39 +196,46 @@ export default {
v-for="item in sortableFields" v-for="item in sortableFields"
ref="packageListSortItem" ref="packageListSortItem"
:key="item.key" :key="item.key"
@click="onSortItemClick(item.key)" @click="onSortItemClick(item.orderBy)"
> >
{{ item.label }} {{ item.label }}
</gl-sorting-item> </gl-sorting-item>
</gl-sorting> </gl-sorting>
<gl-table <gl-table :items="list" :fields="headerFields" :no-local-sorting="true" stacked="md">
ref="packageListTable"
:items="list"
:fields="headerFields"
:no-local-sorting="true"
stacked="md"
>
<template #cell(name)="{value, item}"> <template #cell(name)="{value, item}">
<div <div
class="flex-truncate-parent d-flex align-items-center justify-content-end justify-content-md-start" class="flex-truncate-parent d-flex align-items-center justify-content-end justify-content-md-start"
> >
<a :href="item._links.web_path" data-qa-selector="package_link"> <gl-link
v-gl-tooltip.hover
:title="value"
class="flex-truncate-child"
:href="item._links.web_path"
data-qa-selector="package_link"
>
{{ value }} {{ value }}
</a> </gl-link>
<package-tags
v-if="item.tags && item.tags.length"
class="prepend-left-8"
:tags="item.tags"
hide-label
:tag-display-limit="1"
/>
</div> </div>
<package-tags
v-if="item.tags && item.tags.length"
class="prepend-left-8"
:tags="item.tags"
hide-label
:tag-display-limit="1"
/>
</template> </template>
<template #cell(project)="{value}"> <template #cell(project_path)="{item}">
<div ref="col-project" class="flex-truncate-parent"> <div ref="col-project" class="flex-truncate-parent">
<a :href="value" class="flex-truncate-child"> {{ value }} </a> <gl-link
v-gl-tooltip.hover
:title="item.projectPathName"
:href="item.project_path"
class="flex-truncate-child"
>
{{ item.projectPathName }}
</gl-link>
</div> </div>
</template> </template>
<template #cell(version)="{value}"> <template #cell(version)="{value}">
...@@ -224,6 +248,7 @@ export default { ...@@ -224,6 +248,7 @@ export default {
<time-ago-tooltip :time="value" /> <time-ago-tooltip :time="value" />
</template> </template>
<template #cell(actions)="{item}"> <template #cell(actions)="{item}">
<!-- _links contains the urls needed to navigate to the page details and to perform a package deletion and it comes straight from the API -->
<gl-button <gl-button
ref="action-delete" ref="action-delete"
variant="danger" variant="danger"
...@@ -232,12 +257,11 @@ export default { ...@@ -232,12 +257,11 @@ export default {
:disabled="!item._links.delete_api_path" :disabled="!item._links.delete_api_path"
@click="setItemToBeDeleted(item)" @click="setItemToBeDeleted(item)"
> >
<icon name="remove" /> <gl-icon name="remove" />
</gl-button> </gl-button>
</template> </template>
</gl-table> </gl-table>
<gl-pagination <gl-pagination
ref="packageListPagination"
v-model="currentPage" v-model="currentPage"
:per-page="perPage" :per-page="perPage"
:total-items="totalItems" :total-items="totalItems"
...@@ -247,7 +271,7 @@ export default { ...@@ -247,7 +271,7 @@ export default {
<gl-modal <gl-modal
ref="packageListDeleteModal" ref="packageListDeleteModal"
:modal-id="modalId" modal-id="confirm-delete-pacakge"
ok-variant="danger" ok-variant="danger"
@ok="deleteItemConfirmation" @ok="deleteItemConfirmation"
@cancel="deleteItemCanceled" @cancel="deleteItemCanceled"
......
...@@ -47,7 +47,12 @@ export default { ...@@ -47,7 +47,12 @@ export default {
<template> <template>
<gl-loading-icon v-if="isLoading" class="mt-2" /> <gl-loading-icon v-if="isLoading" class="mt-2" />
<package-list v-else @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> <package-list
v-else
@page:changed="onPageChanged"
@package:delete="onPackageDeleteRequest"
@sort:changed="requestPackagesList"
>
<template #empty-state> <template #empty-state>
<gl-empty-state <gl-empty-state
:title="s__('PackageRegistry|There are no packages yet')" :title="s__('PackageRegistry|There are no packages yet')"
......
...@@ -13,7 +13,7 @@ export const DEFAULT_PAGE_SIZE = 20; ...@@ -13,7 +13,7 @@ export const DEFAULT_PAGE_SIZE = 20;
export const GROUP_PAGE_TYPE = 'groups'; export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_NAME = 'name'; export const LIST_KEY_NAME = 'name';
export const LIST_KEY_PROJECT = 'project'; export const LIST_KEY_PROJECT = 'project_path';
export const LIST_KEY_VERSION = 'version'; export const LIST_KEY_VERSION = 'version';
export const LIST_KEY_PACKAGE_TYPE = 'package_type'; export const LIST_KEY_PACKAGE_TYPE = 'package_type';
export const LIST_KEY_CREATED_AT = 'created_at'; export const LIST_KEY_CREATED_AT = 'created_at';
...@@ -26,5 +26,10 @@ export const LIST_LABEL_PACKAGE_TYPE = __('Type'); ...@@ -26,5 +26,10 @@ export const LIST_LABEL_PACKAGE_TYPE = __('Type');
export const LIST_LABEL_CREATED_AT = __('Created'); export const LIST_LABEL_CREATED_AT = __('Created');
export const LIST_LABEL_ACTIONS = ''; export const LIST_LABEL_ACTIONS = '';
export const LIST_ORDER_BY_PACKAGE_TYPE = 'type';
export const ASCENDING_ODER = 'asc';
export const DESCENDING_ORDER = 'desc';
// The following is not translated because it is used to build a JavaScript exception error message // The following is not translated because it is used to build a JavaScript exception error message
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link'; export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data); export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_PACKAGE_LIST_SUCCESS, data); commit(types.SET_PACKAGE_LIST_SUCCESS, data);
...@@ -22,9 +23,12 @@ export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { ...@@ -22,9 +23,12 @@ export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
export const requestPackagesList = ({ dispatch, state }, pagination = {}) => { export const requestPackagesList = ({ dispatch, state }, pagination = {}) => {
dispatch('setLoading', true); dispatch('setLoading', true);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination; const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = pagination;
const { sort, orderBy } = state.sorting;
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages'; const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
return Api[apiMethod](state.config.resourceId, { params: { page, per_page: perPage } }) return Api[apiMethod](state.config.resourceId, {
params: { page, per_page, sort, order_by: orderBy },
})
.then(({ data, headers }) => { .then(({ data, headers }) => {
dispatch('receivePackagesListSuccess', { data, headers }); dispatch('receivePackagesListSuccess', { data, headers });
}) })
......
import { LIST_KEY_PROJECT } from '../constants';
import { beautifyPath } from '../../shared/utils';
// eslint-disable-next-line import/prefer-default-export
export const getList = state =>
state.packages.map(p => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) }));
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
...@@ -9,6 +10,7 @@ Vue.use(Vuex); ...@@ -9,6 +10,7 @@ Vue.use(Vuex);
export const createStore = () => export const createStore = () =>
new Vuex.Store({ new Vuex.Store({
state, state,
getters,
actions, actions,
mutations, mutations,
}); });
......
...@@ -3,3 +3,4 @@ export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; ...@@ -3,3 +3,4 @@ export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS'; export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION'; export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING';
...@@ -22,4 +22,8 @@ export default { ...@@ -22,4 +22,8 @@ export default {
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
state.pagination = parseIntPagination(normalizedHeaders); state.pagination = parseIntPagination(normalizedHeaders);
}, },
[types.SET_SORTING](state, sorting) {
state.sorting = { ...state.sorting, ...sorting };
},
}; };
...@@ -32,4 +32,15 @@ export default () => ({ ...@@ -32,4 +32,15 @@ export default () => ({
* } * }
*/ */
pagination: {}, pagination: {},
/**
* Sorting object has the following structure:
* {
* sort: String,
* orderBy: String
* }
*/
sorting: {
sort: 'desc',
orderBy: 'version',
},
}); });
...@@ -7,6 +7,8 @@ export const PackageType = { ...@@ -7,6 +7,8 @@ export const PackageType = {
export const TrackingActions = { export const TrackingActions = {
DELETE_PACKAGE: 'delete_package', DELETE_PACKAGE: 'delete_package',
REQUEST_DELETE_PACKAGE: 'request_delete_package',
CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
PULL_PACKAGE: 'pull_package', PULL_PACKAGE: 'pull_package',
}; };
......
import { TrackingCategories } from './constants'; import { TrackingCategories } from './constants';
// eslint-disable-next-line import/prefer-default-export
export const packageTypeToTrackCategory = type => export const packageTypeToTrackCategory = type =>
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
`UI::${TrackingCategories[type]}`; `UI::${TrackingCategories[type]}`;
export const beautifyPath = path => (path ? path.split('/').join(' / ') : '');
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import PackageListApp from 'ee/packages/list/components/packages_list_app.vue'; import PackageListApp from 'ee/packages/list/components/packages_list_app.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_list_app', () => { describe('packages_list_app', () => {
let wrapper; 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>' };
const emptyListHelpUrl = 'helpUrl'; const emptyListHelpUrl = 'helpUrl';
const findListComponent = () => wrapper.find({ name: 'package-list' }); const findListComponent = () => wrapper.find(PackageList);
const findLoadingComponent = () => wrapper.find({ name: 'gl-loading-icon' }); const findLoadingComponent = () => wrapper.find(GlLoadingIcon);
const componentConfig = { const mountComponent = () => {
stubs: { wrapper = shallowMount(PackageListApp, {
GlEmptyState, localVue,
'package-list': { store,
name: 'package-list', stubs: {
template: '<div><slot name="empty-state"></slot></div>', GlEmptyState,
GlLoadingIcon,
PackageList,
}, },
'gl-loading-icon': { name: 'gl-loading-icon', template: '<div>loading</div>' }, });
},
computed: {
isLoading: () => false,
emptyListIllustration: () => 'helpSvg',
emptyListHelpUrl: () => emptyListHelpUrl,
resourceId: () => 'project_id',
},
methods: {
requestPackagesList: jest.fn(),
requestDeletePackage: jest.fn(),
setProjectId: jest.fn(),
setGroupId: jest.fn(),
setUserCanDelete: jest.fn(),
},
}; };
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(PackageListApp, componentConfig); store = new Vuex.Store({
state: {
isLoading: false,
config: {
resourceId: 'project_id',
emptyListIllustration: 'helpSvg',
emptyListHelpUrl: 'helpUrl',
},
},
});
store.dispatch = jest.fn();
}); });
afterEach(() => { afterEach(() => {
...@@ -42,42 +51,52 @@ describe('packages_list_app', () => { ...@@ -42,42 +51,52 @@ describe('packages_list_app', () => {
}); });
it('renders', () => { it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(PackageListApp, { store.state.isLoading = true;
...componentConfig, mountComponent();
computed: {
isLoading: () => true,
},
});
}); });
it('shows the loading component', () => { it('shows the loading component', () => {
const loader = findLoadingComponent(); const loader = findLoadingComponent();
expect(loader.exists()).toBe(true); expect(loader.exists()).toBe(true);
}); });
}); });
it('generate the correct empty list link', () => { describe('when isLoading is false', () => {
const emptyState = findListComponent(); beforeEach(() => {
const link = emptyState.find('a'); mountComponent();
});
expect(link.html()).toMatchInlineSnapshot( it('generate the correct empty list link', () => {
`"<a href=\\"${emptyListHelpUrl}\\" target=\\"_blank\\">publish and share your packages</a>"`, const emptyState = findListComponent();
); const link = emptyState.find('a');
});
it('call requestPackagesList on page:changed', () => { expect(link.html()).toMatchInlineSnapshot(
const list = findListComponent(); `"<a href=\\"${emptyListHelpUrl}\\" target=\\"_blank\\">publish and share your packages</a>"`,
list.vm.$emit('page:changed', 1); );
expect(componentConfig.methods.requestPackagesList).toHaveBeenCalledWith({ page: 1 }); });
});
it('call requestDeletePackage on package:delete', () => { it('call requestPackagesList on page:changed', () => {
const list = findListComponent(); const list = findListComponent();
list.vm.$emit('package:delete', 'foo'); list.vm.$emit('page:changed', 1);
expect(componentConfig.methods.requestDeletePackage).toHaveBeenCalledWith('foo'); expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
});
it('call requestDeletePackage on package:delete', () => {
const list = findListComponent();
list.vm.$emit('package:delete', 'foo');
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
it('calls requestPackagesList on sort:changed', () => {
const list = findListComponent();
list.vm.$emit('sort:changed');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
}); });
}); });
import Vue from 'vue'; import Vuex from 'vuex';
import _ from 'underscore'; import { last } from 'lodash';
import { GlTable, GlSorting, GlPagination, GlModal } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { mount } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import PackagesList from 'ee/packages/list/components/packages_list.vue'; import PackagesList from 'ee/packages/list/components/packages_list.vue';
import PackageTags from 'ee/packages/shared/components/package_tags.vue'; import PackageTags from 'ee/packages/shared/components/package_tags.vue';
import * as SharedUtils from 'ee/packages/shared/utils'; import * as SharedUtils from 'ee/packages/shared/utils';
...@@ -9,55 +10,76 @@ import { TrackingActions } from 'ee/packages/shared/constants'; ...@@ -9,55 +10,76 @@ import { TrackingActions } from 'ee/packages/shared/constants';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import { packageList } from '../../mock_data'; import { packageList } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('packages_list', () => { describe('packages_list', () => {
let wrapper; let wrapper;
let store;
let state;
let getListSpy;
const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findFirstActionColumn = () => wrapper.find({ ref: 'action-delete' }); const findFirstActionColumn = () => wrapper.find({ ref: 'action-delete' });
const findPackageListTable = () => wrapper.find({ ref: 'packageListTable' }); const findPackageListTable = () => wrapper.find(GlTable);
const findPackageListSorting = () => wrapper.find({ ref: 'packageListSorting' }); const findPackageListSorting = () => wrapper.find(GlSorting);
const findPackageListPagination = () => wrapper.find({ ref: 'packageListPagination' }); const findPackageListPagination = () => wrapper.find(GlPagination);
const findPackageListDeleteModal = () => wrapper.find({ ref: 'packageListDeleteModal' }); const findPackageListDeleteModal = () => wrapper.find(GlModal);
const findSortingItems = () => wrapper.findAll({ name: 'sorting-item-stub' }); const findSortingItems = () => wrapper.findAll(GlSortingItem);
const findFirstProjectColumn = () => wrapper.find({ ref: 'col-project' }); const findFirstProjectColumn = () => wrapper.find({ ref: 'col-project' });
const findPackageTags = () => wrapper.findAll(PackageTags); const findPackageTags = () => wrapper.findAll(PackageTags);
const findEmptySlot = () => wrapper.find({ name: 'empty-slot-stub' });
const mountOptions = { const mountComponent = options => {
stubs: { wrapper = mount(PackagesList, {
...stubChildren(PackagesList), localVue,
GlTable: false, store,
GlSortingItem: { name: 'sorting-item-stub', template: '<div><slot></slot></div>' }, stubs: {
}, ...stubChildren(PackagesList),
computed: { GlTable,
list: () => [...packageList], GlSortingItem,
perPage: () => 1, },
totalItems: () => 1, ...options,
page: () => 1, });
canDestroyPackage: () => true,
isGroupPage: () => false,
},
}; };
beforeEach(() => { beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests getListSpy = jest.fn();
// see https://github.com/vuejs/vue-test-utils/issues/532 getListSpy.mockReturnValue(packageList);
Vue.config.silent = true; state = {
wrapper = mount(PackagesList, mountOptions); packages: [...packageList],
pagination: {
perPage: 1,
total: 1,
page: 1,
},
config: {
isGroupPage: false,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
};
store = new Vuex.Store({
state,
getters: {
getList: getListSpy,
},
});
store.dispatch = jest.fn();
}); });
afterEach(() => { afterEach(() => {
Vue.config.silent = false;
wrapper.destroy(); wrapper.destroy();
}); });
describe('when is isGroupPage', () => { describe('when is isGroupPage', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mount(PackagesList, { state.config.isGroupPage = true;
...mountOptions, mountComponent();
computed: {
...mountOptions.computed,
isGroupPage: () => true,
},
});
}); });
it('has project field', () => { it('has project field', () => {
...@@ -71,31 +93,41 @@ describe('packages_list', () => { ...@@ -71,31 +93,41 @@ describe('packages_list', () => {
}); });
}); });
it('contains a sorting component', () => { describe('layout', () => {
const sorting = findPackageListSorting(); beforeEach(() => {
expect(sorting.exists()).toBe(true); mountComponent();
}); });
it('contains a table component', () => { it('contains a sorting component', () => {
const sorting = findPackageListTable(); const sorting = findPackageListSorting();
expect(sorting.exists()).toBe(true); expect(sorting.exists()).toBe(true);
}); });
it('contains a pagination component', () => { it('contains a table component', () => {
const sorting = findPackageListPagination(); const sorting = findPackageListTable();
expect(sorting.exists()).toBe(true); expect(sorting.exists()).toBe(true);
}); });
it('contains a modal component', () => { it('contains a pagination component', () => {
const sorting = findPackageListDeleteModal(); const sorting = findPackageListPagination();
expect(sorting.exists()).toBe(true); expect(sorting.exists()).toBe(true);
}); });
it('renders package tags when a package has tags', () => { it('contains a modal component', () => {
expect(findPackageTags()).toHaveLength(1); const sorting = findPackageListDeleteModal();
expect(sorting.exists()).toBe(true);
});
it('renders package tags when a package has tags', () => {
expect(findPackageTags()).toHaveLength(1);
});
}); });
describe('when the user can destroy the package', () => { describe('when the user can destroy the package', () => {
beforeEach(() => {
mountComponent();
});
it('show the action column', () => { it('show the action column', () => {
const action = findFirstActionColumn(); const action = findFirstActionColumn();
expect(action.exists()).toBe(true); expect(action.exists()).toBe(true);
...@@ -112,10 +144,10 @@ describe('packages_list', () => { ...@@ -112,10 +144,10 @@ describe('packages_list', () => {
it('delete button set itemToBeDeleted and open the modal', () => { it('delete button set itemToBeDeleted and open the modal', () => {
wrapper.vm.$refs.packageListDeleteModal.show = jest.fn(); wrapper.vm.$refs.packageListDeleteModal.show = jest.fn();
const item = _.last(packageList); const item = last(wrapper.vm.list);
const action = findFirstActionColumn(); const action = findFirstActionColumn();
action.vm.$emit('click'); action.vm.$emit('click');
return Vue.nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.itemToBeDeleted).toEqual(item); expect(wrapper.vm.itemToBeDeleted).toEqual(item);
expect(wrapper.vm.$refs.packageListDeleteModal.show).toHaveBeenCalled(); expect(wrapper.vm.$refs.packageListDeleteModal.show).toHaveBeenCalled();
}); });
...@@ -144,14 +176,11 @@ describe('packages_list', () => { ...@@ -144,14 +176,11 @@ describe('packages_list', () => {
}); });
describe('when the list is empty', () => { describe('when the list is empty', () => {
const findEmptySlot = () => wrapper.find({ name: 'empty-slot-stub' });
beforeEach(() => { beforeEach(() => {
wrapper = mount(PackagesList, { getListSpy.mockReturnValue([]);
...mountOptions, mountComponent({
computed: { list: () => [] },
slots: { slots: {
'empty-state': { name: 'empty-slot-stub', template: '<div>bar</div>' }, 'empty-state': EmptySlotStub,
}, },
}); });
}); });
...@@ -165,17 +194,56 @@ describe('packages_list', () => { ...@@ -165,17 +194,56 @@ describe('packages_list', () => {
}); });
describe('sorting component', () => { describe('sorting component', () => {
let sorting;
let sortingItems;
beforeEach(() => {
mountComponent();
sorting = findPackageListSorting();
sortingItems = findSortingItems();
});
it('has all the sortable items', () => { it('has all the sortable items', () => {
const sortingItems = findSortingItems();
expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length); expect(sortingItems.length).toEqual(wrapper.vm.sortableFields.length);
}); });
it('on sort change set sorting in vuex and emit event', () => {
sorting.vm.$emit('sortDirectionChange');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
it('on sort item click set sorting and emit event', () => {
const item = sortingItems.at(0);
const { orderBy } = wrapper.vm.sortableFields[0];
item.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
expect(wrapper.emitted('sort:changed')).toBeTruthy();
});
});
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', () => { it('emits page:changed events when the page changes', () => {
wrapper.vm.currentPage = 2; pagination.vm.$emit(modelEvent, 2);
expect(wrapper.emitted('page:changed')).toEqual([[2]]); expect(wrapper.emitted('page:changed')).toEqual([[2]]);
}); });
}); });
describe('table component', () => { describe('table component', () => {
beforeEach(() => {
mountComponent();
});
it('has stacked-md class', () => { it('has stacked-md class', () => {
const table = findPackageListTable(); const table = findPackageListTable();
expect(table.classes()).toContain('b-table-stacked-md'); expect(table.classes()).toContain('b-table-stacked-md');
...@@ -188,6 +256,7 @@ describe('packages_list', () => { ...@@ -188,6 +256,7 @@ describe('packages_list', () => {
const category = 'foo'; const category = 'foo';
beforeEach(() => { beforeEach(() => {
mountComponent();
eventSpy = jest.spyOn(Tracking, 'event'); eventSpy = jest.spyOn(Tracking, 'event');
utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category); utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } }); wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
......
...@@ -28,11 +28,15 @@ describe('Actions Package list store', () => { ...@@ -28,11 +28,15 @@ describe('Actions Package list store', () => {
}); });
describe('requestPackagesList', () => { describe('requestPackagesList', () => {
const sorting = {
sort: 'asc',
orderBy: 'version',
};
it('should fetch the project packages list when isGroupPage is false', done => { it('should fetch the project packages list when isGroupPage is false', done => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: false, resourceId: 1 } }, { config: { isGroupPage: false, resourceId: 1 }, sorting },
[], [],
[ [
{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: true },
...@@ -41,7 +45,7 @@ describe('Actions Package list store', () => { ...@@ -41,7 +45,7 @@ describe('Actions Package list store', () => {
], ],
() => { () => {
expect(Api.projectPackages).toHaveBeenCalledWith(1, { expect(Api.projectPackages).toHaveBeenCalledWith(1, {
params: { page: 1, per_page: 20 }, params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
}); });
done(); done();
}, },
...@@ -52,7 +56,7 @@ describe('Actions Package list store', () => { ...@@ -52,7 +56,7 @@ describe('Actions Package list store', () => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: true, resourceId: 2 } }, { config: { isGroupPage: true, resourceId: 2 }, sorting },
[], [],
[ [
{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: true },
...@@ -61,7 +65,7 @@ describe('Actions Package list store', () => { ...@@ -61,7 +65,7 @@ describe('Actions Package list store', () => {
], ],
() => { () => {
expect(Api.groupPackages).toHaveBeenCalledWith(2, { expect(Api.groupPackages).toHaveBeenCalledWith(2, {
params: { page: 1, per_page: 20 }, params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
}); });
done(); done();
}, },
...@@ -73,7 +77,7 @@ describe('Actions Package list store', () => { ...@@ -73,7 +77,7 @@ describe('Actions Package list store', () => {
testAction( testAction(
actions.requestPackagesList, actions.requestPackagesList,
undefined, undefined,
{ config: { isGroupPage: false, resourceId: 2 } }, { config: { isGroupPage: false, resourceId: 2 }, sorting },
[], [],
[{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: false }], [{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: false }],
() => { () => {
...@@ -179,4 +183,17 @@ describe('Actions Package list store', () => { ...@@ -179,4 +183,17 @@ describe('Actions Package list store', () => {
}); });
}); });
}); });
describe('setSorting', () => {
it('should commit SET_SORTING', done => {
testAction(
actions.setSorting,
'foo',
null,
[{ type: types.SET_SORTING, payload: 'foo' }],
[],
done,
);
});
});
}); });
import * as getters from 'ee/packages/list/stores/getters';
import { packageList } from '../../mock_data';
describe('Getters registry list store', () => {
const state = {
packages: packageList,
};
describe('getList', () => {
const result = getters.getList(state);
it('returns a list of packages', () => {
expect(result).toHaveLength(packageList.length);
expect(result[0].name).toBe('Test package');
});
it('adds projectPathName', () => {
expect(result[0].projectPathName).toMatchInlineSnapshot(`"foo / bar / baz"`);
});
});
});
...@@ -65,4 +65,16 @@ describe('Mutations Registry Store', () => { ...@@ -65,4 +65,16 @@ describe('Mutations Registry Store', () => {
expect(mockState.pagination).toEqual(mockPagination); expect(mockState.pagination).toEqual(mockPagination);
}); });
}); });
describe('SET_SORTING', () => {
it('should merge the sorting object with sort value', () => {
mutations[types.SET_SORTING](mockState, { sort: 'desc' });
expect(mockState.sorting).toEqual({ ...mockState.sorting, sort: 'desc' });
});
it('should merge the sorting object with order_by value', () => {
mutations[types.SET_SORTING](mockState, { orderBy: 'foo' });
expect(mockState.sorting).toEqual({ ...mockState.sorting, orderBy: 'foo' });
});
});
}); });
...@@ -13,6 +13,7 @@ export const mavenPackage = { ...@@ -13,6 +13,7 @@ export const mavenPackage = {
}, },
name: 'Test package', name: 'Test package',
package_type: 'maven', package_type: 'maven',
project_path: 'foo/bar/baz',
project_id: 1, project_id: 1,
updated_at: '2015-12-10', updated_at: '2015-12-10',
version: '1.0.0', version: '1.0.0',
...@@ -41,6 +42,7 @@ export const npmPackage = { ...@@ -41,6 +42,7 @@ export const npmPackage = {
id: 2, id: 2,
name: '@Test/package', name: '@Test/package',
package_type: 'npm', package_type: 'npm',
project_path: 'foo/bar/baz',
project_id: 1, project_id: 1,
updated_at: '2015-12-10', updated_at: '2015-12-10',
version: '', version: '',
...@@ -68,6 +70,7 @@ export const conanPackage = { ...@@ -68,6 +70,7 @@ export const conanPackage = {
created_at: '2015-12-10', created_at: '2015-12-10',
id: 3, id: 3,
name: 'conan-package', name: 'conan-package',
project_path: 'foo/bar/baz',
package_files: [], package_files: [],
package_type: 'conan', package_type: 'conan',
project_id: 1, project_id: 1,
......
import { packageTypeToTrackCategory } from 'ee/packages/shared/utils'; import { packageTypeToTrackCategory, beautifyPath } from 'ee/packages/shared/utils';
import { PackageType, TrackingCategories } from 'ee/packages/shared/constants'; import { PackageType, TrackingCategories } from 'ee/packages/shared/constants';
describe('Packages shared utils', () => { describe('Packages shared utils', () => {
...@@ -14,4 +14,12 @@ describe('Packages shared utils', () => { ...@@ -14,4 +14,12 @@ describe('Packages shared utils', () => {
); );
}); });
}); });
describe('beautifyPath', () => {
it('returns a string with spaces around /', () => {
expect(beautifyPath('foo/bar')).toBe('foo / bar');
});
it('does not fail for empty string', () => {
expect(beautifyPath()).toBe('');
});
});
}); });
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