diff options
Diffstat (limited to 'spec/frontend/groups_projects/components/transfer_locations_spec.js')
-rw-r--r-- | spec/frontend/groups_projects/components/transfer_locations_spec.js | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js new file mode 100644 index 00000000000..74424ee3230 --- /dev/null +++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js @@ -0,0 +1,377 @@ +import { + GlDropdown, + GlDropdownItem, + GlAlert, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json'; +import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json'; +import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { __ } from '~/locale'; +import TransferLocations from '~/groups_projects/components/transfer_locations.vue'; +import { getTransferLocations } from '~/api/projects_api'; +import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; + +jest.mock('~/api/projects_api', () => ({ + getTransferLocations: jest.fn(), +})); + +describe('TransferLocations', () => { + let wrapper; + + // Default data + const resourceId = '1'; + const defaultPropsData = { + groupTransferLocationsApiMethod: getTransferLocations, + value: null, + }; + const additionalDropdownItem = { + id: -1, + humanName: __('No parent group'), + }; + + // Mock requests + const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse); + const mockResolvedGetTransferLocations = ({ + data = transferLocationsResponsePage1, + page = '1', + nextPage = '2', + total = '4', + totalPages = '2', + prevPage = null, + } = {}) => { + getTransferLocations.mockResolvedValueOnce({ + data, + headers: { + 'x-per-page': '2', + 'x-page': page, + 'x-total': total, + 'x-total-pages': totalPages, + 'x-next-page': nextPage, + 'x-prev-page': prevPage, + }, + }); + }; + const mockRejectedGetTransferLocations = () => { + const error = new Error(); + + getTransferLocations.mockRejectedValueOnce(error); + }; + + // VTU wrapper helpers + Vue.use(VueApollo); + const createComponent = ({ + propsData = {}, + requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]], + } = {}) => { + wrapper = mountExtended(TransferLocations, { + provide: { + resourceId, + }, + propsData: { + ...defaultPropsData, + ...propsData, + }, + apolloProvider: createMockApollo(requestHandlers), + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const showDropdown = async () => { + findDropdown().vm.$emit('show'); + await waitForPromises(); + }; + const findUserTransferLocations = () => + wrapper + .findByTestId('user-transfer-locations') + .findAllComponents(GlDropdownItem) + .wrappers.map((dropdownItem) => dropdownItem.text()); + const findGroupTransferLocations = () => + wrapper + .findByTestId('group-transfer-locations') + .findAllComponents(GlDropdownItem) + .wrappers.map((dropdownItem) => dropdownItem.text()); + const findDropdownItemByText = (text) => + wrapper + .findAllComponents(GlDropdownItem) + .wrappers.find((dropdownItem) => dropdownItem.text() === text); + const findAlert = () => wrapper.findComponent(GlAlert); + const findSearch = () => wrapper.findComponent(GlSearchBoxByType); + const searchEmitInput = (searchTerm = 'foo') => findSearch().vm.$emit('input', searchTerm); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when `GlDropdown` is opened', () => { + it('shows loading icon', async () => { + getTransferLocations.mockReturnValueOnce(new Promise(() => {})); + createComponent(); + findDropdown().vm.$emit('show'); + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('fetches and renders user and group transfer locations', async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + + const { namespace } = currentUserNamespaceQueryResponse.data.currentUser; + + expect(findUserTransferLocations()).toEqual([namespace.fullName]); + expect(findGroupTransferLocations()).toEqual( + transferLocationsResponsePage1.map((transferLocation) => transferLocation.full_name), + ); + }); + + describe('when `showUserTransferLocations` prop is `false`', () => { + it('does not fetch user transfer locations', async () => { + mockResolvedGetTransferLocations(); + createComponent({ + propsData: { + showUserTransferLocations: false, + }, + }); + await showDropdown(); + + expect(wrapper.findByTestId('user-transfer-locations').exists()).toBe(false); + }); + }); + + describe('when `additionalDropdownItems` prop is passed', () => { + it('displays additional dropdown items', async () => { + mockResolvedGetTransferLocations(); + createComponent({ + propsData: { + additionalDropdownItems: [additionalDropdownItem], + }, + }); + await showDropdown(); + + expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true); + }); + + describe('when loading', () => { + it('does not display additional dropdown items', async () => { + getTransferLocations.mockReturnValueOnce(new Promise(() => {})); + createComponent({ + propsData: { + additionalDropdownItems: [additionalDropdownItem], + }, + }); + findDropdown().vm.$emit('show'); + await nextTick(); + + expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined(); + }); + }); + }); + + describe('when transfer locations have already been fetched', () => { + beforeEach(async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + }); + + it('does not fetch transfer locations', async () => { + getTransferLocations.mockClear(); + defaultQueryHandler.mockClear(); + + await showDropdown(); + + expect(getTransferLocations).not.toHaveBeenCalled(); + expect(defaultQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when `getTransferLocations` API call fails', () => { + it('displays dismissible error alert', async () => { + mockRejectedGetTransferLocations(); + createComponent(); + await showDropdown(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + + alert.vm.$emit('dismiss'); + await nextTick(); + + expect(alert.exists()).toBe(false); + }); + }); + + describe('when `currentUser` GraphQL query fails', () => { + it('displays error alert', async () => { + mockResolvedGetTransferLocations(); + const error = new Error(); + createComponent({ + requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]], + }); + await showDropdown(); + + expect(findAlert().exists()).toBe(true); + }); + }); + }); + + describe('when transfer location is selected', () => { + it('displays transfer location as selected', () => { + const [{ id, full_name: humanName }] = transferLocationsResponsePage1; + + createComponent({ + propsData: { + value: { + id, + humanName, + }, + }, + }); + + expect(findDropdown().props('text')).toBe(humanName); + }); + }); + + describe('when search is typed in', () => { + const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]]; + + const arrange = async ({ propsData, searchTerm } = {}) => { + mockResolvedGetTransferLocations(); + createComponent({ propsData }); + await showDropdown(); + mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch }); + searchEmitInput(searchTerm); + await nextTick(); + }; + + it('sets `isSearchLoading` prop to `true`', async () => { + await arrange(); + + expect(findSearch().props('isLoading')).toBe(true); + }); + + it('passes `search` param to API call and updates group transfer locations', async () => { + await arrange(); + + await waitForPromises(); + + expect(getTransferLocations).toHaveBeenCalledWith( + resourceId, + expect.objectContaining({ search: 'foo' }), + ); + expect(findGroupTransferLocations()).toEqual( + transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name), + ); + }); + + it('does not display additional dropdown items if they do not match the search', async () => { + await arrange({ + propsData: { + additionalDropdownItems: [additionalDropdownItem], + }, + }); + await waitForPromises(); + + expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined(); + }); + + it('displays additional dropdown items if they match the search', async () => { + await arrange({ + propsData: { + additionalDropdownItems: [additionalDropdownItem], + }, + searchTerm: 'No par', + }); + await waitForPromises(); + + expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true); + }); + }); + + describe('when there are no more pages', () => { + it('does not show intersection observer', async () => { + mockResolvedGetTransferLocations({ + data: transferLocationsResponsePage1, + nextPage: null, + total: '2', + totalPages: '1', + prevPage: null, + }); + createComponent(); + await showDropdown(); + + expect(findIntersectionObserver().exists()).toBe(false); + }); + }); + + describe('when intersection observer appears', () => { + const arrange = async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + + mockResolvedGetTransferLocations({ + data: transferLocationsResponsePage2, + page: '2', + nextPage: null, + prevPage: '1', + totalPages: '2', + }); + + intersectionObserverEmitAppear(); + await nextTick(); + }; + + it('shows loading icon', async () => { + await arrange(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('passes `page` param to API call', async () => { + await arrange(); + + await waitForPromises(); + + expect(getTransferLocations).toHaveBeenCalledWith( + resourceId, + expect.objectContaining({ page: 2 }), + ); + }); + + it('updates dropdown with new group transfer locations', async () => { + await arrange(); + + await waitForPromises(); + + expect(findGroupTransferLocations()).toEqual( + [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map( + ({ full_name: fullName }) => fullName, + ), + ); + }); + }); + + describe('when `label` prop is passed', () => { + it('renders label', () => { + const label = 'Foo bar'; + + createComponent({ propsData: { label } }); + + expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true); + }); + }); +}); |