diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-26 15:10:41 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-26 15:10:41 +0300 |
commit | 04f9cef437b65b4a62624936a37a99cfbfb4d61c (patch) | |
tree | 9edb887220b45ecd69f2aefa22a0fea09ed03ee1 /spec/frontend/user_lists | |
parent | 47d07def1648ffc0787fe92ea5e351ccc5e9c4a4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/user_lists')
4 files changed, 617 insertions, 0 deletions
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js new file mode 100644 index 00000000000..7a33c6faac9 --- /dev/null +++ b/spec/frontend/user_lists/components/user_lists_spec.js @@ -0,0 +1,195 @@ +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; +import { within } from '@testing-library/dom'; +import { mount, createWrapper } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import waitForPromises from 'helpers/wait_for_promises'; +import Api from '~/api'; +import UserListsComponent from '~/user_lists/components/user_lists.vue'; +import UserListsTable from '~/user_lists/components/user_lists_table.vue'; +import createStore from '~/user_lists/store/index'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { userList } from '../../feature_flags/mock_data'; + +jest.mock('~/api'); + +Vue.use(Vuex); + +describe('~/user_lists/components/user_lists.vue', () => { + const mockProvide = { + newUserListPath: '/user-lists/new', + featureFlagsHelpPagePath: '/help/feature-flags', + errorStateSvgPath: '/assets/illustrations/feature_flag.svg', + }; + + const mockState = { + projectId: '1', + }; + + let wrapper; + let store; + + const factory = (provide = mockProvide, fn = mount) => { + store = createStore(mockState); + wrapper = fn(UserListsComponent, { + store, + provide, + }); + }; + + const newButton = () => within(wrapper.element).queryAllByText('New user list'); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('without permissions', () => { + const provideData = { + ...mockProvide, + newUserListPath: null, + }; + + beforeEach(() => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} }); + factory(provideData); + }); + + it('does not render new user list button', () => { + expect(newButton()).toHaveLength(0); + }); + }); + + describe('loading state', () => { + it('renders a loading icon', () => { + Api.fetchFeatureFlagUserLists.mockReturnValue(new Promise(() => {})); + + factory(); + + const loadingElement = wrapper.findComponent(GlLoadingIcon); + + expect(loadingElement.exists()).toBe(true); + expect(loadingElement.props('label')).toEqual('Loading user lists'); + }); + }); + + describe('successful request', () => { + describe('without user lists', () => { + let emptyState; + + beforeEach(async () => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} }); + + factory(); + await waitForPromises(); + await Vue.nextTick(); + + emptyState = wrapper.findComponent(GlEmptyState); + }); + + it('should render the empty state', async () => { + expect(emptyState.exists()).toBe(true); + }); + + it('renders new feature flag button', () => { + expect(newButton()).not.toHaveLength(0); + }); + + it('renders generic title', () => { + const title = createWrapper( + within(emptyState.element).getByText('Get started with user lists'), + ); + expect(title.exists()).toBe(true); + }); + + it('renders generic description', () => { + const description = createWrapper( + within(emptyState.element).getByText( + 'User lists allow you to define a set of users to use with Feature Flags.', + ), + ); + expect(description.exists()).toBe(true); + }); + }); + + describe('with paginated user lists', () => { + let table; + + beforeEach(async () => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ + data: [userList], + headers: { + 'x-next-page': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '5', + }, + }); + + factory(); + jest.spyOn(store, 'dispatch'); + await Vue.nextTick(); + table = wrapper.findComponent(UserListsTable); + }); + + it('should render a table with feature flags', () => { + expect(table.exists()).toBe(true); + expect(table.props('userLists')).toEqual([userList]); + }); + + it('renders new feature flag button', () => { + expect(newButton()).not.toHaveLength(0); + }); + + describe('pagination', () => { + let pagination; + + beforeEach(() => { + pagination = wrapper.findComponent(TablePagination); + }); + + it('should render pagination', () => { + expect(pagination.exists()).toBe(true); + }); + + it('should make an API request when page is clicked', () => { + jest.spyOn(store, 'dispatch'); + pagination.vm.change('4'); + + expect(store.dispatch).toHaveBeenCalledWith('setUserListsOptions', { + page: '4', + }); + }); + }); + }); + }); + + describe('unsuccessful request', () => { + beforeEach(async () => { + Api.fetchFeatureFlagUserLists.mockRejectedValue(); + factory(); + + await Vue.nextTick(); + }); + + it('should render error state', () => { + const emptyState = wrapper.findComponent(GlEmptyState); + const title = createWrapper( + within(emptyState.element).getByText('There was an error fetching the user lists.'), + ); + expect(title.exists()).toBe(true); + const description = createWrapper( + within(emptyState.element).getByText( + 'Try again in a few moments or contact your support team.', + ), + ); + expect(description.exists()).toBe(true); + }); + + it('renders new feature flag button', () => { + expect(newButton()).not.toHaveLength(0); + }); + }); +}); diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js new file mode 100644 index 00000000000..925a52ee562 --- /dev/null +++ b/spec/frontend/user_lists/components/user_lists_table_spec.js @@ -0,0 +1,98 @@ +import { GlModal } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import * as timeago from 'timeago.js'; +import UserListsTable from '~/user_lists/components/user_lists_table.vue'; +import { userList } from '../../feature_flags/mock_data'; + +jest.mock('timeago.js', () => ({ + format: jest.fn().mockReturnValue('2 weeks ago'), + register: jest.fn(), +})); + +describe('User Lists Table', () => { + let wrapper; + let userLists; + + beforeEach(() => { + userLists = new Array(5).fill(userList).map((x, i) => ({ ...x, id: i })); + wrapper = mount(UserListsTable, { + propsData: { userLists }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should display the details of a user list', () => { + expect(wrapper.find('[data-testid="ffUserListName"]').text()).toBe(userList.name); + expect(wrapper.find('[data-testid="ffUserListIds"]').text()).toBe( + userList.user_xids.replace(/,/g, ', '), + ); + expect(wrapper.find('[data-testid="ffUserListTimestamp"]').text()).toBe('created 2 weeks ago'); + expect(timeago.format).toHaveBeenCalledWith(userList.created_at); + }); + + it('should set the title for a tooltip on the created stamp', () => { + expect(wrapper.find('[data-testid="ffUserListTimestamp"]').attributes('title')).toBe( + 'Feb 4, 2020 8:13am GMT+0000', + ); + }); + + it('should display a user list entry per user list', () => { + const lists = wrapper.findAll('[data-testid="ffUserList"]'); + expect(lists).toHaveLength(5); + lists.wrappers.forEach((list) => { + expect(list.find('[data-testid="ffUserListName"]').exists()).toBe(true); + expect(list.find('[data-testid="ffUserListIds"]').exists()).toBe(true); + expect(list.find('[data-testid="ffUserListTimestamp"]').exists()).toBe(true); + }); + }); + + describe('edit button', () => { + it('should link to the path for the user list', () => { + expect(wrapper.find('[data-testid="edit-user-list"]').attributes('href')).toBe(userList.path); + }); + }); + + describe('delete button', () => { + it('should display the confirmation modal', () => { + const modal = wrapper.find(GlModal); + + wrapper.find('[data-testid="delete-user-list"]').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(modal.text()).toContain(`Delete ${userList.name}?`); + expect(modal.text()).toContain(`User list ${userList.name} will be removed.`); + }); + }); + }); + + describe('confirmation modal', () => { + let modal; + + beforeEach(() => { + modal = wrapper.find(GlModal); + + wrapper.find('button').trigger('click'); + + return wrapper.vm.$nextTick(); + }); + + it('should emit delete with list on confirmation', () => { + modal.find('[data-testid="modal-confirm"]').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('delete')).toEqual([[userLists[0]]]); + }); + }); + + it('should not emit delete with list when not confirmed', () => { + modal.find('button').trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted('delete')).toBeUndefined(); + }); + }); + }); +}); diff --git a/spec/frontend/user_lists/store/index/actions_spec.js b/spec/frontend/user_lists/store/index/actions_spec.js new file mode 100644 index 00000000000..c5d7d557de9 --- /dev/null +++ b/spec/frontend/user_lists/store/index/actions_spec.js @@ -0,0 +1,203 @@ +import testAction from 'helpers/vuex_action_helper'; +import Api from '~/api'; +import { + setUserListsOptions, + requestUserLists, + receiveUserListsSuccess, + receiveUserListsError, + fetchUserLists, + deleteUserList, + receiveDeleteUserListError, + clearAlert, +} from '~/user_lists/store/index/actions'; +import * as types from '~/user_lists/store/index/mutation_types'; +import createState from '~/user_lists/store/index/state'; +import { userList } from '../../../feature_flags/mock_data'; + +jest.mock('~/api.js'); + +describe('~/user_lists/store/index/actions', () => { + let state; + + beforeEach(() => { + state = createState({ projectId: '1' }); + }); + + describe('setUserListsOptions', () => { + it('should commit SET_USER_LISTS_OPTIONS mutation', (done) => { + testAction( + setUserListsOptions, + { page: '1', scope: 'all' }, + state, + [{ type: types.SET_USER_LISTS_OPTIONS, payload: { page: '1', scope: 'all' } }], + [], + done, + ); + }); + }); + + describe('fetchUserLists', () => { + beforeEach(() => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} }); + }); + + describe('success', () => { + it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => { + testAction( + fetchUserLists, + null, + state, + [], + [ + { + type: 'requestUserLists', + }, + { + payload: { data: [userList], headers: {} }, + type: 'receiveUserListsSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestUserLists and receiveUserListsError ', (done) => { + Api.fetchFeatureFlagUserLists.mockRejectedValue(); + + testAction( + fetchUserLists, + null, + state, + [], + [ + { + type: 'requestUserLists', + }, + { + type: 'receiveUserListsError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestUserLists', () => { + it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => { + testAction(requestUserLists, null, state, [{ type: types.REQUEST_USER_LISTS }], [], done); + }); + }); + + describe('receiveUserListsSuccess', () => { + it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => { + testAction( + receiveUserListsSuccess, + { data: [userList], headers: {} }, + state, + [ + { + type: types.RECEIVE_USER_LISTS_SUCCESS, + payload: { data: [userList], headers: {} }, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveUserListsError', () => { + it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => { + testAction( + receiveUserListsError, + null, + state, + [{ type: types.RECEIVE_USER_LISTS_ERROR }], + [], + done, + ); + }); + }); + + describe('deleteUserList', () => { + beforeEach(() => { + state.userLists = [userList]; + }); + + describe('success', () => { + beforeEach(() => { + Api.deleteFeatureFlagUserList.mockResolvedValue(); + }); + + it('should refresh the user lists', (done) => { + testAction( + deleteUserList, + userList, + state, + [], + [{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } }); + }); + + it('should dispatch receiveDeleteUserListError', (done) => { + testAction( + deleteUserList, + userList, + state, + [], + [ + { type: 'requestDeleteUserList', payload: userList }, + { + type: 'receiveDeleteUserListError', + payload: { list: userList, error: 'some error' }, + }, + ], + done, + ); + }); + }); + }); + + describe('receiveDeleteUserListError', () => { + it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => { + testAction( + receiveDeleteUserListError, + { list: userList, error: 'mock error' }, + state, + [ + { + type: 'RECEIVE_DELETE_USER_LIST_ERROR', + payload: { list: userList, error: 'mock error' }, + }, + ], + [], + done, + ); + }); + }); + + describe('clearAlert', () => { + it('should commit RECEIVE_CLEAR_ALERT', (done) => { + const alertIndex = 3; + + testAction( + clearAlert, + alertIndex, + state, + [{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/user_lists/store/index/mutations_spec.js b/spec/frontend/user_lists/store/index/mutations_spec.js new file mode 100644 index 00000000000..370838ae5fb --- /dev/null +++ b/spec/frontend/user_lists/store/index/mutations_spec.js @@ -0,0 +1,121 @@ +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import * as types from '~/user_lists/store/index/mutation_types'; +import mutations from '~/user_lists/store/index/mutations'; +import createState from '~/user_lists/store/index/state'; +import { userList } from '../../../feature_flags/mock_data'; + +describe('~/user_lists/store/index/mutations', () => { + let state; + + beforeEach(() => { + state = createState({ projectId: '1' }); + }); + + describe('SET_USER_LISTS_OPTIONS', () => { + it('should set provided options', () => { + mutations[types.SET_USER_LISTS_OPTIONS](state, { page: '1', scope: 'all' }); + + expect(state.options).toEqual({ page: '1', scope: 'all' }); + }); + }); + + describe('REQUEST_USER_LISTS', () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_USER_LISTS](state); + expect(state.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_USER_LISTS_SUCCESS', () => { + const headers = { + 'x-next-page': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '5', + }; + + beforeEach(() => { + mutations[types.RECEIVE_USER_LISTS_SUCCESS](state, { data: [userList], headers }); + }); + + it('sets isLoading to false', () => { + expect(state.isLoading).toBe(false); + }); + + it('sets userLists to the received userLists', () => { + expect(state.userLists).toEqual([userList]); + }); + + it('sets pagination info for user lits', () => { + expect(state.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers))); + }); + + it('sets the count for user lists', () => { + expect(state.count).toBe(parseInt(headers['X-TOTAL'], 10)); + }); + }); + + describe('RECEIVE_USER_LISTS_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_USER_LISTS_ERROR](state); + }); + + it('should set isLoading to false', () => { + expect(state.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(state.hasError).toEqual(true); + }); + }); + + describe('REQUEST_DELETE_USER_LIST', () => { + beforeEach(() => { + state.userLists = [userList]; + mutations[types.REQUEST_DELETE_USER_LIST](state, userList); + }); + + it('should remove the deleted list', () => { + expect(state.userLists).not.toContain(userList); + }); + }); + + describe('RECEIVE_DELETE_USER_LIST_ERROR', () => { + beforeEach(() => { + state.userLists = []; + mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](state, { + list: userList, + error: 'some error', + }); + }); + + it('should set isLoading to false and hasError to false', () => { + expect(state.isLoading).toBe(false); + expect(state.hasError).toBe(false); + }); + + it('should add the user list back to the list of user lists', () => { + expect(state.userLists).toContain(userList); + }); + }); + + describe('RECEIVE_CLEAR_ALERT', () => { + it('clears the alert', () => { + state.alerts = ['a server error']; + + mutations[types.RECEIVE_CLEAR_ALERT](state, 0); + + expect(state.alerts).toEqual([]); + }); + + it('clears the alert at the specified index', () => { + state.alerts = ['a server error', 'another error', 'final error']; + + mutations[types.RECEIVE_CLEAR_ALERT](state, 1); + + expect(state.alerts).toEqual(['a server error', 'final error']); + }); + }); +}); |