415 lines
12 KiB
JavaScript
415 lines
12 KiB
JavaScript
import VueApollo from 'vue-apollo';
|
|
import Vue from 'vue';
|
|
import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
|
|
import organizationGroupsGraphQlResponse from 'test_fixtures/graphql/organizations/groups.query.graphql.json';
|
|
import GroupsView from '~/organizations/shared/components/groups_view.vue';
|
|
import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
|
|
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
|
|
import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue';
|
|
import {
|
|
renderDeleteSuccessToast,
|
|
deleteParams,
|
|
formatGroups,
|
|
} from 'ee_else_ce/organizations/shared/utils';
|
|
import { deleteGroup } from 'ee_else_ce/api/groups_api';
|
|
import groupsQuery from '~/organizations/shared/graphql/queries/groups.query.graphql';
|
|
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
|
|
import { TIMESTAMP_TYPE_CREATED_AT } from '~/vue_shared/components/resource_lists/constants';
|
|
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
|
|
import { createAlert } from '~/alert';
|
|
import { DEFAULT_PER_PAGE } from '~/api';
|
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
|
import waitForPromises from 'helpers/wait_for_promises';
|
|
import {
|
|
pageInfoMultiplePages,
|
|
pageInfoEmpty,
|
|
pageInfoOnePage,
|
|
} from 'jest/organizations/mock_data';
|
|
|
|
const {
|
|
data: {
|
|
organization: {
|
|
groups: { nodes },
|
|
},
|
|
},
|
|
} = organizationGroupsGraphQlResponse;
|
|
|
|
const MOCK_DELETE_PARAMS = {
|
|
testParam: true,
|
|
};
|
|
|
|
jest.mock('ee_else_ce/organizations/shared/utils', () => ({
|
|
...jest.requireActual('ee_else_ce/organizations/shared/utils'),
|
|
renderDeleteSuccessToast: jest.fn(),
|
|
deleteParams: jest.fn(() => MOCK_DELETE_PARAMS),
|
|
}));
|
|
jest.mock('~/alert');
|
|
jest.mock('ee_else_ce/api/groups_api');
|
|
|
|
Vue.use(VueApollo);
|
|
|
|
describe('GroupsView', () => {
|
|
let wrapper;
|
|
let mockApollo;
|
|
|
|
const defaultProvide = {
|
|
groupsEmptyStateSvgPath: 'illustrations/empty-state/empty-groups-md.svg',
|
|
newGroupPath: '/groups/new',
|
|
organizationGid: 'gid://gitlab/Organizations::Organization/1',
|
|
};
|
|
|
|
const defaultPropsData = {
|
|
listItemClass: 'gl-px-5',
|
|
search: 'foo',
|
|
sortName: SORT_ITEM_NAME.value,
|
|
sortDirection: SORT_DIRECTION_ASC,
|
|
};
|
|
|
|
const groups = {
|
|
nodes,
|
|
pageInfo: pageInfoMultiplePages,
|
|
};
|
|
|
|
const successHandler = jest.fn().mockResolvedValue({
|
|
data: {
|
|
organization: {
|
|
id: defaultProvide.organizationGid,
|
|
groups,
|
|
},
|
|
},
|
|
});
|
|
|
|
const createComponent = ({ handler = successHandler, propsData = {} } = {}) => {
|
|
mockApollo = createMockApollo([[groupsQuery, handler]]);
|
|
|
|
wrapper = shallowMountExtended(GroupsView, {
|
|
apolloProvider: mockApollo,
|
|
provide: defaultProvide,
|
|
propsData: {
|
|
...defaultPropsData,
|
|
...propsData,
|
|
},
|
|
});
|
|
};
|
|
|
|
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
|
|
const findNewGroupButton = () => wrapper.findComponent(NewGroupButton);
|
|
const findGroupsList = () => wrapper.findComponent(GroupsList);
|
|
const findGroupsListByGroupId = (groupId) =>
|
|
findGroupsList()
|
|
.props('groups')
|
|
.find((group) => group.id === groupId);
|
|
|
|
afterEach(() => {
|
|
mockApollo = null;
|
|
});
|
|
|
|
describe('when API call is loading', () => {
|
|
it('renders loading icon', () => {
|
|
createComponent();
|
|
|
|
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('when API call is successful', () => {
|
|
describe.each`
|
|
shouldShowEmptyStateButtons
|
|
${false}
|
|
${true}
|
|
`(
|
|
'when there are no groups and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`',
|
|
({ shouldShowEmptyStateButtons }) => {
|
|
const emptyHandler = jest.fn().mockResolvedValue({
|
|
data: {
|
|
organization: {
|
|
id: defaultProvide.organizationGid,
|
|
groups: {
|
|
nodes: [],
|
|
pageInfo: pageInfoEmpty,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
it(`renders empty state ${
|
|
shouldShowEmptyStateButtons ? 'with' : 'without'
|
|
} buttons`, async () => {
|
|
createComponent({
|
|
handler: emptyHandler,
|
|
propsData: { shouldShowEmptyStateButtons },
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(wrapper.findComponent(GroupsAndProjectsEmptyState).props()).toMatchObject({
|
|
title: "You don't have any groups yet.",
|
|
description:
|
|
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
|
|
svgPath: defaultProvide.groupsEmptyStateSvgPath,
|
|
search: 'foo',
|
|
});
|
|
|
|
expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe('when there are groups', () => {
|
|
beforeEach(() => {
|
|
createComponent({ propsData: {} });
|
|
});
|
|
|
|
it('calls GraphQL query with correct variables', async () => {
|
|
await waitForPromises();
|
|
|
|
expect(successHandler).toHaveBeenCalledWith({
|
|
id: defaultProvide.organizationGid,
|
|
search: defaultPropsData.search,
|
|
sort: 'name_asc',
|
|
last: null,
|
|
first: DEFAULT_PER_PAGE,
|
|
before: null,
|
|
after: null,
|
|
});
|
|
});
|
|
|
|
it('renders `GroupsList` component and passes correct props', async () => {
|
|
await waitForPromises();
|
|
|
|
expect(findGroupsList().props()).toMatchObject({
|
|
groups: formatGroups(nodes),
|
|
showGroupIcon: true,
|
|
listItemClass: defaultPropsData.listItemClass,
|
|
timestampType: TIMESTAMP_TYPE_CREATED_AT,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when there is one page of groups', () => {
|
|
beforeEach(async () => {
|
|
createComponent({
|
|
handler: jest.fn().mockResolvedValue({
|
|
data: {
|
|
organization: {
|
|
id: defaultProvide.organizationGid,
|
|
groups: {
|
|
nodes,
|
|
pageInfo: pageInfoOnePage,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('does not render pagination', () => {
|
|
expect(findPagination().exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when there is a next page of groups', () => {
|
|
const mockEndCursor = 'mockEndCursor';
|
|
|
|
beforeEach(async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('renders pagination', () => {
|
|
expect(findPagination().exists()).toBe(true);
|
|
});
|
|
|
|
describe('when next button is clicked', () => {
|
|
beforeEach(() => {
|
|
findPagination().vm.$emit('next', mockEndCursor);
|
|
});
|
|
|
|
it('emits `page-change` event', () => {
|
|
expect(wrapper.emitted('page-change')[0]).toEqual([
|
|
{
|
|
endCursor: mockEndCursor,
|
|
startCursor: null,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when `endCursor` prop is changed', () => {
|
|
beforeEach(() => {
|
|
wrapper.setProps({ endCursor: mockEndCursor });
|
|
});
|
|
|
|
it('calls query with correct variables', () => {
|
|
expect(successHandler).toHaveBeenCalledWith({
|
|
after: mockEndCursor,
|
|
before: null,
|
|
first: DEFAULT_PER_PAGE,
|
|
id: defaultProvide.organizationGid,
|
|
last: null,
|
|
search: defaultPropsData.search,
|
|
sort: 'name_asc',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when there is a previous page of groups', () => {
|
|
const mockStartCursor = 'mockStartCursor';
|
|
|
|
beforeEach(async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('renders pagination', () => {
|
|
expect(findPagination().exists()).toBe(true);
|
|
});
|
|
|
|
describe('when previous button is clicked', () => {
|
|
beforeEach(async () => {
|
|
findPagination().vm.$emit('prev', mockStartCursor);
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('emits `page-change` event', () => {
|
|
expect(wrapper.emitted('page-change')[0]).toEqual([
|
|
{
|
|
endCursor: null,
|
|
startCursor: mockStartCursor,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when `startCursor` prop is changed', () => {
|
|
beforeEach(async () => {
|
|
wrapper.setProps({ startCursor: mockStartCursor });
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('calls query with correct variables', () => {
|
|
expect(successHandler).toHaveBeenCalledWith({
|
|
after: null,
|
|
before: mockStartCursor,
|
|
first: null,
|
|
id: defaultProvide.organizationGid,
|
|
last: DEFAULT_PER_PAGE,
|
|
search: defaultPropsData.search,
|
|
sort: 'name_asc',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when API call is not successful', () => {
|
|
const error = new Error();
|
|
|
|
beforeEach(() => {
|
|
createComponent({ handler: jest.fn().mockRejectedValue(error) });
|
|
});
|
|
|
|
it('displays error alert', async () => {
|
|
await waitForPromises();
|
|
|
|
expect(createAlert).toHaveBeenCalledWith({
|
|
message: GroupsView.i18n.errorMessage,
|
|
error,
|
|
captureError: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Deleting group', () => {
|
|
const MOCK_GROUP = formatGroups(nodes)[0];
|
|
|
|
describe('when API call is successful', () => {
|
|
beforeEach(async () => {
|
|
deleteGroup.mockResolvedValueOnce(Promise.resolve());
|
|
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('calls deleteGroup, properly sets loading state, and refetches list when promise resolves', async () => {
|
|
findGroupsList().vm.$emit('delete', MOCK_GROUP);
|
|
|
|
expect(deleteParams).toHaveBeenCalledWith(MOCK_GROUP);
|
|
expect(deleteGroup).toHaveBeenCalledWith(MOCK_GROUP.id, MOCK_DELETE_PARAMS);
|
|
expect(findGroupsListByGroupId(MOCK_GROUP.id).actionLoadingStates[ACTION_DELETE]).toBe(
|
|
true,
|
|
);
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findGroupsListByGroupId(MOCK_GROUP.id).actionLoadingStates[ACTION_DELETE]).toBe(
|
|
false,
|
|
);
|
|
// Refetches list
|
|
expect(successHandler).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('does call renderDeleteSuccessToast', async () => {
|
|
findGroupsList().vm.$emit('delete', MOCK_GROUP);
|
|
await waitForPromises();
|
|
|
|
expect(renderDeleteSuccessToast).toHaveBeenCalledWith(MOCK_GROUP, 'Group');
|
|
});
|
|
|
|
it('does not call createAlert', async () => {
|
|
findGroupsList().vm.$emit('delete', MOCK_GROUP);
|
|
await waitForPromises();
|
|
|
|
expect(createAlert).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('when API call is not successful', () => {
|
|
const error = new Error();
|
|
|
|
beforeEach(async () => {
|
|
deleteGroup.mockRejectedValue(error);
|
|
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
});
|
|
|
|
it('calls deleteGroup, properly sets loading state, and shows error alert', async () => {
|
|
findGroupsList().vm.$emit('delete', MOCK_GROUP);
|
|
|
|
expect(deleteParams).toHaveBeenCalledWith(MOCK_GROUP);
|
|
expect(deleteGroup).toHaveBeenCalledWith(MOCK_GROUP.id, MOCK_DELETE_PARAMS);
|
|
expect(findGroupsListByGroupId(MOCK_GROUP.id).actionLoadingStates[ACTION_DELETE]).toBe(
|
|
true,
|
|
);
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findGroupsListByGroupId(MOCK_GROUP.id).actionLoadingStates[ACTION_DELETE]).toBe(
|
|
false,
|
|
);
|
|
|
|
// Does not refetch list
|
|
expect(successHandler).toHaveBeenCalledTimes(1);
|
|
expect(createAlert).toHaveBeenCalledWith({
|
|
message: 'An error occurred deleting the group. Please refresh the page to try again.',
|
|
error,
|
|
captureError: true,
|
|
});
|
|
});
|
|
|
|
it('does not call renderDeleteSuccessToast', async () => {
|
|
findGroupsList().vm.$emit('delete', MOCK_GROUP);
|
|
await waitForPromises();
|
|
|
|
expect(renderDeleteSuccessToast).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
});
|