summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components/user_select_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/vue_shared/components/user_select_spec.js')
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js311
1 files changed, 311 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
new file mode 100644
index 00000000000..5a609568220
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -0,0 +1,311 @@
+import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import { ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
+import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
+import {
+ searchResponse,
+ projectMembersResponse,
+ participantsQueryResponse,
+} from '../../sidebar/mock_data';
+
+const assignee = {
+ id: 'gid://gitlab/User/4',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Developer',
+ username: 'dev',
+ webUrl: '/dev',
+ status: null,
+};
+
+const mockError = jest.fn().mockRejectedValue('Error!');
+
+const waitForSearch = async () => {
+ jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
+};
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('User select dropdown', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
+ const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
+ const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
+ const findUnselectedParticipants = () =>
+ wrapper.findAll('[data-testid="unselected-participant"]');
+ const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
+ const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
+ const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
+
+ const createComponent = ({
+ props = {},
+ searchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse),
+ participantsQueryHandler = jest.fn().mockResolvedValue(participantsQueryResponse),
+ } = {}) => {
+ fakeApollo = createMockApollo([
+ [searchUsersQuery, searchQueryHandler],
+ [getIssueParticipantsQuery, participantsQueryHandler],
+ ]);
+ wrapper = shallowMount(UserSelect, {
+ localVue,
+ apolloProvider: fakeApollo,
+ propsData: {
+ headerText: 'test',
+ text: 'test-text',
+ fullPath: '/project',
+ iid: '1',
+ value: [],
+ currentUser: {
+ username: 'random',
+ name: 'Mr. Random',
+ },
+ allowMultipleAssignees: false,
+ ...props,
+ },
+ stubs: {
+ GlDropdown,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('renders a loading spinner if participants are loading', () => {
+ createComponent();
+
+ expect(findParticipantsLoading().exists()).toBe(true);
+ });
+
+ it('emits an `error` event if participants query was rejected', async () => {
+ createComponent({ participantsQueryHandler: mockError });
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[], []]);
+ });
+
+ it('emits an `error` event if search query was rejected', async () => {
+ createComponent({ searchQueryHandler: mockError });
+ await waitForSearch();
+
+ expect(wrapper.emitted('error')).toEqual([[], []]);
+ });
+
+ it('renders current user if they are not in participants or assignees', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findCurrentUser().exists()).toBe(true);
+ });
+
+ it('displays correct amount of selected users', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ await waitForPromises();
+
+ expect(findSelectedParticipants()).toHaveLength(1);
+ });
+
+ describe('when search is empty', () => {
+ it('renders a merged list of participants and project members', async () => {
+ createComponent();
+ await waitForPromises();
+ expect(findUnselectedParticipants()).toHaveLength(3);
+ });
+
+ it('renders `Unassigned` link with the checkmark when there are no selected users', async () => {
+ createComponent();
+ await waitForPromises();
+ expect(findUnassignLink().props('isChecked')).toBe(true);
+ });
+
+ it('renders `Unassigned` link without the checkmark when there are selected users', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ await waitForPromises();
+ expect(findUnassignLink().props('isChecked')).toBe(false);
+ });
+
+ it('emits an input event with empty array after clicking on `Unassigned`', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ await waitForPromises();
+ findUnassignLink().vm.$emit('click');
+
+ expect(wrapper.emitted('input')).toEqual([[[]]]);
+ });
+
+ it('emits an empty array after unselecting the only selected assignee', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ await waitForPromises();
+
+ findSelectedParticipants().at(0).vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('input')).toEqual([[[]]]);
+ });
+
+ it('allows only one user to be selected if `allowMultipleAssignees` is false', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ await waitForPromises();
+
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ expect(wrapper.emitted('input')).toEqual([
+ [
+ [
+ {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ status: null,
+ username: 'root',
+ webUrl: '/root',
+ },
+ ],
+ ],
+ ]);
+ });
+
+ it('adds user to selected if `allowMultipleAssignees` is true', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ allowMultipleAssignees: true,
+ },
+ });
+ await waitForPromises();
+
+ findUnselectedParticipants().at(0).vm.$emit('click');
+ expect(wrapper.emitted('input')[0][0]).toHaveLength(2);
+ });
+ });
+
+ describe('when searching', () => {
+ it('does not show loading spinner when debounce timer is still running', async () => {
+ createComponent();
+ await waitForPromises();
+ findSearchField().vm.$emit('input', 'roo');
+
+ expect(findParticipantsLoading().exists()).toBe(false);
+ });
+
+ it('shows loading spinner when searching for users', async () => {
+ createComponent();
+ await waitForPromises();
+ findSearchField().vm.$emit('input', 'roo');
+ jest.advanceTimersByTime(ASSIGNEES_DEBOUNCE_DELAY);
+ await nextTick();
+
+ expect(findParticipantsLoading().exists()).toBe(true);
+ });
+
+ it('renders a list of found users and external participants matching search term', async () => {
+ createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
+ await waitForPromises();
+
+ findSearchField().vm.$emit('input', 'ro');
+ await waitForSearch();
+
+ expect(findUnselectedParticipants()).toHaveLength(3);
+ });
+
+ it('renders a list of found users only if no external participants match search term', async () => {
+ createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
+ await waitForPromises();
+
+ findSearchField().vm.$emit('input', 'roo');
+ await waitForSearch();
+
+ expect(findUnselectedParticipants()).toHaveLength(2);
+ });
+
+ it('shows a message about no matches if search returned an empty list', async () => {
+ const responseCopy = cloneDeep(searchResponse);
+ responseCopy.data.workspace.users.nodes = [];
+
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue(responseCopy),
+ });
+ await waitForPromises();
+ findSearchField().vm.$emit('input', 'tango');
+ await waitForSearch();
+
+ expect(findUnselectedParticipants()).toHaveLength(0);
+ expect(findEmptySearchResults().exists()).toBe(true);
+ });
+ });
+
+ // TODO Remove this test after the following issue is resolved in the backend
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ describe('temporary error suppression', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ const nullError = { message: 'Cannot return null for non-nullable field GroupMember.user' };
+
+ it.each`
+ mockErrors
+ ${[nullError]}
+ ${[nullError, nullError]}
+ `('does not emit errors', async ({ mockErrors }) => {
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue({
+ errors: mockErrors,
+ }),
+ });
+ await waitForSearch();
+
+ expect(wrapper.emitted()).toEqual({});
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it.each`
+ mockErrors
+ ${[{ message: 'serious error' }]}
+ ${[nullError, { message: 'serious error' }]}
+ `('emits error when non-null related errors are included', async ({ mockErrors }) => {
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue({
+ errors: mockErrors,
+ }),
+ });
+ await waitForSearch();
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ });
+ });
+});