401 lines
12 KiB
JavaScript
401 lines
12 KiB
JavaScript
import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
|
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|
import { nextTick } from 'vue';
|
|
import VueApollo from 'vue-apollo';
|
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
|
import waitForPromises from 'helpers/wait_for_promises';
|
|
import createFlash from '~/flash';
|
|
import { IssuableType } from '~/issue_show/constants';
|
|
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
|
|
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
|
|
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
|
|
import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
|
|
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
|
import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
|
|
import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
|
|
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
|
|
import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data';
|
|
|
|
jest.mock('~/flash');
|
|
|
|
const updateIssueAssigneesMutationSuccess = jest
|
|
.fn()
|
|
.mockResolvedValue(updateIssueAssigneesMutationResponse);
|
|
const mockError = jest.fn().mockRejectedValue('Error!');
|
|
|
|
const localVue = createLocalVue();
|
|
localVue.use(VueApollo);
|
|
|
|
const initialAssignees = [
|
|
{
|
|
id: 'some-user',
|
|
avatarUrl: 'some-user-avatar',
|
|
name: 'test',
|
|
username: 'test',
|
|
webUrl: '/test',
|
|
},
|
|
];
|
|
|
|
describe('Sidebar assignees widget', () => {
|
|
let wrapper;
|
|
let fakeApollo;
|
|
|
|
const findAssignees = () => wrapper.findComponent(IssuableAssignees);
|
|
const findRealtimeAssignees = () => wrapper.findComponent(SidebarAssigneesRealtime);
|
|
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
|
|
const findInviteMembersLink = () => wrapper.findComponent(SidebarInviteMembers);
|
|
const findUserSelect = () => wrapper.findComponent(UserSelect);
|
|
|
|
const expandDropdown = () => wrapper.vm.$refs.toggle.expand();
|
|
|
|
const createComponent = ({
|
|
issuableQueryHandler = jest.fn().mockResolvedValue(issuableQueryResponse),
|
|
updateIssueAssigneesMutationHandler = updateIssueAssigneesMutationSuccess,
|
|
props = {},
|
|
provide = {},
|
|
} = {}) => {
|
|
fakeApollo = createMockApollo([
|
|
[getIssueAssigneesQuery, issuableQueryHandler],
|
|
[updateIssueAssigneesMutation, updateIssueAssigneesMutationHandler],
|
|
]);
|
|
wrapper = shallowMount(SidebarAssigneesWidget, {
|
|
localVue,
|
|
apolloProvider: fakeApollo,
|
|
propsData: {
|
|
iid: '1',
|
|
issuableId: 0,
|
|
fullPath: '/mygroup/myProject',
|
|
allowMultipleAssignees: true,
|
|
...props,
|
|
},
|
|
provide: {
|
|
canUpdate: true,
|
|
rootPath: '/',
|
|
...provide,
|
|
},
|
|
stubs: {
|
|
SidebarEditableItem,
|
|
UserSelect,
|
|
GlSearchBoxByType,
|
|
GlDropdown,
|
|
},
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
gon.current_username = 'root';
|
|
gon.current_user_fullname = 'Administrator';
|
|
gon.current_user_avatar_url = '/root';
|
|
});
|
|
|
|
afterEach(() => {
|
|
wrapper.destroy();
|
|
wrapper = null;
|
|
fakeApollo = null;
|
|
delete gon.current_username;
|
|
});
|
|
|
|
describe('with passed initial assignees', () => {
|
|
it('passes `initialLoading` as false to editable item', () => {
|
|
createComponent({
|
|
props: {
|
|
initialAssignees,
|
|
},
|
|
});
|
|
|
|
expect(findEditableItem().props('initialLoading')).toBe(false);
|
|
});
|
|
|
|
it('renders an initial assignees list with initialAssignees prop', () => {
|
|
createComponent({
|
|
props: {
|
|
initialAssignees,
|
|
},
|
|
});
|
|
|
|
expect(findAssignees().props('users')).toEqual(initialAssignees);
|
|
});
|
|
|
|
it('renders a collapsible item title calculated with initial assignees length', () => {
|
|
createComponent({
|
|
props: {
|
|
initialAssignees,
|
|
},
|
|
});
|
|
|
|
expect(findEditableItem().props('title')).toBe('Assignee');
|
|
});
|
|
});
|
|
|
|
describe('without passed initial assignees', () => {
|
|
it('passes `initialLoading` as true to editable item', () => {
|
|
createComponent();
|
|
|
|
expect(findEditableItem().props('initialLoading')).toBe(true);
|
|
});
|
|
|
|
it('renders assignees list from API response when resolved', async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
|
|
expect(findAssignees().props('users')).toEqual(
|
|
issuableQueryResponse.data.workspace.issuable.assignees.nodes,
|
|
);
|
|
});
|
|
|
|
it('renders an error when issuable query is rejected', async () => {
|
|
createComponent({
|
|
issuableQueryHandler: mockError,
|
|
});
|
|
await waitForPromises();
|
|
|
|
expect(createFlash).toHaveBeenCalledWith({
|
|
message: 'An error occurred while fetching participants.',
|
|
});
|
|
});
|
|
|
|
it('assigns current user when clicking `Assign self`', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
findAssignees().vm.$emit('assign-self');
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
|
|
assigneeUsernames: ['root'],
|
|
fullPath: '/mygroup/myProject',
|
|
iid: '1',
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(
|
|
findAssignees()
|
|
.props('users')
|
|
.some((user) => user.username === 'root'),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('emits an event with assignees list and issuable id on successful mutation', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
findAssignees().vm.$emit('assign-self');
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
|
|
assigneeUsernames: ['root'],
|
|
fullPath: '/mygroup/myProject',
|
|
iid: '1',
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(wrapper.emitted('assignees-updated')).toEqual([
|
|
[
|
|
{
|
|
assignees: [
|
|
{
|
|
__typename: 'User',
|
|
avatarUrl:
|
|
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
|
id: 'gid://gitlab/User/1',
|
|
name: 'Administrator',
|
|
username: 'root',
|
|
webUrl: '/root',
|
|
status: null,
|
|
},
|
|
],
|
|
id: 'gid://gitlab/Issue/1',
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('does not trigger mutation or fire event when editing and exiting without making changes', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
findEditableItem().vm.$emit('open');
|
|
|
|
await waitForPromises();
|
|
|
|
findEditableItem().vm.$emit('close');
|
|
|
|
expect(findEditableItem().props('isDirty')).toBe(false);
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledTimes(0);
|
|
expect(wrapper.emitted('assignees-updated')).toBe(undefined);
|
|
});
|
|
|
|
describe('when expanded', () => {
|
|
beforeEach(async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
expandDropdown();
|
|
});
|
|
|
|
it('collapses the widget on user select toggle event', async () => {
|
|
findUserSelect().vm.$emit('toggle');
|
|
await nextTick();
|
|
expect(findUserSelect().isVisible()).toBe(false);
|
|
});
|
|
|
|
it('calls an update mutation with correct variables on User Select input event', () => {
|
|
findUserSelect().vm.$emit('input', [{ username: 'root' }]);
|
|
findEditableItem().vm.$emit('close');
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
|
|
assigneeUsernames: ['root'],
|
|
fullPath: '/mygroup/myProject',
|
|
iid: '1',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when multiselect is disabled', () => {
|
|
beforeEach(async () => {
|
|
createComponent({ props: { allowMultipleAssignees: false } });
|
|
await waitForPromises();
|
|
expandDropdown();
|
|
});
|
|
|
|
it('closes a dropdown after User Select input event', async () => {
|
|
findUserSelect().vm.$emit('input', [{ username: 'root' }]);
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
|
|
assigneeUsernames: ['root'],
|
|
fullPath: '/mygroup/myProject',
|
|
iid: '1',
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findUserSelect().isVisible()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('when multiselect is enabled', () => {
|
|
beforeEach(async () => {
|
|
createComponent({ props: { allowMultipleAssignees: true } });
|
|
await waitForPromises();
|
|
expandDropdown();
|
|
});
|
|
|
|
it('does not call a mutation when clicking on participants until dropdown is closed', () => {
|
|
findUserSelect().vm.$emit('input', [{ username: 'root' }]);
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).not.toHaveBeenCalled();
|
|
expect(findUserSelect().isVisible()).toBe(true);
|
|
});
|
|
|
|
it('calls the mutation old issuable id if `iid` prop was changed', async () => {
|
|
findUserSelect().vm.$emit('input', [{ username: 'francina.skiles' }]);
|
|
wrapper.setProps({
|
|
iid: '2',
|
|
});
|
|
await nextTick();
|
|
findEditableItem().vm.$emit('close');
|
|
|
|
expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
|
|
assigneeUsernames: ['francina.skiles'],
|
|
fullPath: '/mygroup/myProject',
|
|
iid: '1',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('shows an error if update assignees mutation is rejected', async () => {
|
|
createComponent({ updateIssueAssigneesMutationHandler: mockError });
|
|
await waitForPromises();
|
|
expandDropdown();
|
|
|
|
findUserSelect().vm.$emit('input', []);
|
|
findEditableItem().vm.$emit('close');
|
|
|
|
await waitForPromises();
|
|
|
|
expect(createFlash).toHaveBeenCalledWith({
|
|
message: 'An error occurred while updating assignees.',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when user is not signed in', () => {
|
|
beforeEach(() => {
|
|
gon.current_username = undefined;
|
|
createComponent();
|
|
});
|
|
|
|
it('passes signedIn prop as false to IssuableAssignees', () => {
|
|
expect(findAssignees().props('signedIn')).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('when realtime feature flag is disabled', async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
expect(findRealtimeAssignees().exists()).toBe(false);
|
|
});
|
|
|
|
it('when realtime feature flag is enabled', async () => {
|
|
createComponent({
|
|
provide: {
|
|
glFeatures: {
|
|
realTimeIssueSidebar: true,
|
|
},
|
|
},
|
|
});
|
|
await waitForPromises();
|
|
expect(findRealtimeAssignees().exists()).toBe(true);
|
|
});
|
|
|
|
describe('when making changes to participants list', () => {
|
|
beforeEach(async () => {
|
|
createComponent();
|
|
});
|
|
|
|
it('passes falsy `isDirty` prop to editable item if no changes to selected users were made', () => {
|
|
expandDropdown();
|
|
expect(findEditableItem().props('isDirty')).toBe(false);
|
|
});
|
|
|
|
it('passes truthy `isDirty` prop after User Select component emitted an input event', async () => {
|
|
expandDropdown();
|
|
expect(findEditableItem().props('isDirty')).toBe(false);
|
|
findUserSelect().vm.$emit('input', []);
|
|
await nextTick();
|
|
expect(findEditableItem().props('isDirty')).toBe(true);
|
|
});
|
|
|
|
it('passes falsy `isDirty` prop after dropdown is closed', async () => {
|
|
expandDropdown();
|
|
findUserSelect().vm.$emit('input', []);
|
|
findEditableItem().vm.$emit('close');
|
|
await waitForPromises();
|
|
expect(findEditableItem().props('isDirty')).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('does not render invite members link on non-issue sidebar', async () => {
|
|
createComponent({ props: { issuableType: IssuableType.MergeRequest } });
|
|
await waitForPromises();
|
|
expect(findInviteMembersLink().exists()).toBe(false);
|
|
});
|
|
|
|
it('does not render invite members link if `directlyInviteMembers` was not passed', async () => {
|
|
createComponent();
|
|
await waitForPromises();
|
|
expect(findInviteMembersLink().exists()).toBe(false);
|
|
});
|
|
|
|
it('renders invite members link if `directlyInviteMembers` is true', async () => {
|
|
createComponent({
|
|
provide: {
|
|
directlyInviteMembers: true,
|
|
},
|
|
});
|
|
await waitForPromises();
|
|
expect(findInviteMembersLink().exists()).toBe(true);
|
|
});
|
|
});
|