Commit 1cc56dbe authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '33905-connect-api-to-frontend' into 'master'

Connect new package API to vue implementation

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