import { GlButton, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { s__ } from '~/locale'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import ListPage from '~/packages_and_registries/package_registry/pages/list.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue'; import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; import { GRAPHQL_PAGE_SIZE, EMPTY_LIST_HELP_URL, PACKAGE_HELP_URL, } from '~/packages_and_registries/package_registry/constants'; import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; import { packagesListQuery, packageData, pagination } from '../mock_data'; jest.mock('~/alert'); describe('PackagesListApp', () => { let wrapper; let apolloProvider; const defaultProvide = { emptyListIllustration: 'emptyListIllustration', isGroupPage: true, fullPath: 'gitlab-org', settingsPath: 'settings-path', }; const PackageList = { name: 'package-list', template: '
', props: OriginalPackageList.props, }; const GlLoadingIcon = { name: 'gl-loading-icon', template: '
loading
' }; const searchPayload = { sort: 'VERSION_DESC', filters: { packageName: 'foo', packageType: 'CONAN', packageVersion: '1.0.1' }, }; const findPackageTitle = () => wrapper.findComponent(PackageTitle); const findSearch = () => wrapper.findComponent(PackageSearch); const findListComponent = () => wrapper.findComponent(PackageList); const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findDeletePackages = () => wrapper.findComponent(DeletePackages); const findSettingsLink = () => wrapper.findComponent(GlButton); const findPagination = () => wrapper.findComponent(PersistedPagination); const mountComponent = ({ resolver = jest.fn().mockResolvedValue(packagesListQuery()), mutationResolver, provide = defaultProvide, } = {}) => { Vue.use(VueApollo); const requestHandlers = [ [getPackagesQuery, resolver], [destroyPackagesMutation, mutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); wrapper = shallowMountExtended(ListPage, { apolloProvider, provide, stubs: { GlEmptyState, GlLoadingIcon, GlSprintf, GlLink, PackageTitle, PackageList, DeletePackages, }, directives: { GlTooltip: createMockDirective('gl-tooltip'), }, }); }; const waitForFirstRequest = () => { // emit a search update so the query is executed findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] }); return waitForPromises(); }; it('does not execute the query without sort being set', () => { const resolver = jest.fn().mockResolvedValue(packagesListQuery()); mountComponent({ resolver }); expect(resolver).not.toHaveBeenCalled(); }); it('has persisted pagination', async () => { const resolver = jest.fn().mockResolvedValue(packagesListQuery()); mountComponent({ resolver }); await waitForFirstRequest(); expect(findPagination().props('pagination')).toEqual(pagination()); }); it('has a package title', async () => { mountComponent(); await waitForFirstRequest(); expect(findPackageTitle().exists()).toBe(true); expect(findPackageTitle().props()).toMatchObject({ count: 2, helpUrl: PACKAGE_HELP_URL, }); }); describe('link to settings', () => { describe('when settings path is not provided', () => { beforeEach(() => { mountComponent({ provide: { ...defaultProvide, settingsPath: '', }, }); }); it('is not rendered', () => { expect(findSettingsLink().exists()).toBe(false); }); }); describe('when settings path is provided', () => { const label = s__('PackageRegistry|Configure in settings'); beforeEach(() => { mountComponent(); }); it('is rendered', () => { expect(findSettingsLink().exists()).toBe(true); }); it('has the right icon', () => { expect(findSettingsLink().props('icon')).toBe('settings'); }); it('has the right attributes', () => { expect(findSettingsLink().attributes()).toMatchObject({ 'aria-label': label, href: defaultProvide.settingsPath, }); }); it('sets tooltip with right label', () => { const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip'); expect(tooltip.value).toBe(label); }); }); }); describe('search component', () => { it('exists', () => { mountComponent(); expect(findSearch().exists()).toBe(true); }); it('on update triggers a new query with updated values', async () => { const resolver = jest.fn().mockResolvedValue(packagesListQuery()); mountComponent({ resolver }); findSearch().vm.$emit('update', searchPayload); await waitForPromises(); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ groupSort: searchPayload.sort, ...searchPayload.filters, }), ); }); }); describe('list component', () => { let resolver; beforeEach(() => { resolver = jest.fn().mockResolvedValue(packagesListQuery()); mountComponent({ resolver }); }); it('exists and has the right props', async () => { await waitForFirstRequest(); expect(findListComponent().props()).toMatchObject({ list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]), isLoading: false, groupSettings: expect.objectContaining({ mavenPackageRequestsForwarding: true, npmPackageRequestsForwarding: true, pypiPackageRequestsForwarding: true, }), }); }); it('when pagination emits next event fetches the next set of records', async () => { await waitForFirstRequest(); findPagination().vm.$emit('next'); await waitForPromises(); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }), ); }); it('when pagination emits prev event fetches the prev set of records', async () => { await waitForFirstRequest(); findPagination().vm.$emit('prev'); await waitForPromises(); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ first: null, before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE, }), ); }); }); describe.each` type | sortType ${WORKSPACE_PROJECT} | ${'sort'} ${WORKSPACE_GROUP} | ${'groupSort'} `('$type query', ({ type, sortType }) => { let provide; let resolver; const isGroupPage = type === WORKSPACE_GROUP; beforeEach(() => { provide = { ...defaultProvide, isGroupPage }; resolver = jest.fn().mockResolvedValue(packagesListQuery({ type })); mountComponent({ provide, resolver }); return waitForFirstRequest(); }); it('succeeds', () => { expect(findPackageTitle().props('count')).toBe(2); }); it('calls the resolver with the right parameters', () => { expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }), ); }); it('list component has group settings prop set', () => { expect(findListComponent().props()).toMatchObject({ groupSettings: expect.objectContaining({ mavenPackageRequestsForwarding: true, npmPackageRequestsForwarding: true, pypiPackageRequestsForwarding: true, }), }); }); }); describe.each` description | resolverResponse ${'empty response'} | ${packagesListQuery({ extend: { nodes: [] } })} ${'error response'} | ${{ data: { group: null } }} `(`$description renders empty state`, ({ resolverResponse }) => { beforeEach(() => { const resolver = jest.fn().mockResolvedValue(resolverResponse); mountComponent({ resolver }); return waitForFirstRequest(); }); it('generate the correct empty list link', () => { const link = findListComponent().findComponent(GlLink); expect(link.attributes('href')).toBe(EMPTY_LIST_HELP_URL); expect(link.text()).toBe('publish and share your packages'); }); it('includes the right content on the default tab', () => { expect(findEmptyState().text()).toContain(ListPage.i18n.emptyPageTitle); }); }); describe('filter without results', () => { beforeEach(async () => { mountComponent(); await waitForFirstRequest(); findSearch().vm.$emit('update', { sort: 'VERSION_DESC', filters: { packageName: 'test', }, }); return nextTick(); }); it('should show specific empty message', () => { expect(findEmptyState().text()).toContain(ListPage.i18n.noResultsTitle); expect(findEmptyState().text()).toContain(ListPage.i18n.widenFilters); }); }); describe('delete packages', () => { it('exists and has the correct props', async () => { mountComponent(); await waitForFirstRequest(); expect(findDeletePackages().props()).toMatchObject({ refetchQueries: [{ query: getPackagesQuery, variables: {} }], showSuccessAlert: true, }); }); it('deletePackages is bound to package-list delete event', async () => { mountComponent(); await waitForFirstRequest(); findListComponent().vm.$emit('delete', [{ id: 1 }]); expect(findDeletePackages().emitted('start')).toEqual([[]]); }); it('start and end event set loading correctly', async () => { mountComponent(); await waitForFirstRequest(); findDeletePackages().vm.$emit('start'); await nextTick(); expect(findListComponent().props('isLoading')).toBe(true); findDeletePackages().vm.$emit('end'); await nextTick(); expect(findListComponent().props('isLoading')).toBe(false); }); }); });