import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlKeysetPagination } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import createMockApollo from 'jest/helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/details.vue'; import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue'; import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue'; import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; import TagsList from '~/registry/explorer/components/details_page/tags_list.vue'; import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue'; import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; import { graphQLImageDetailsMock, graphQLImageDetailsEmptyTagsMock, graphQLDeleteImageRepositoryTagsMock, containerRepositoryMock, tagsMock, tagsPageInfo, } from '../mock_data'; import { DeleteModal } from '../stubs'; const localVue = createLocalVue(); describe('Details Page', () => { let wrapper; let apolloProvider; const findDeleteModal = () => wrapper.find(DeleteModal); const findPagination = () => wrapper.find(GlKeysetPagination); const findTagsLoader = () => wrapper.find(TagsLoader); const findTagsList = () => wrapper.find(TagsList); const findDeleteAlert = () => wrapper.find(DeleteAlert); const findDetailsHeader = () => wrapper.find(DetailsHeader); const findEmptyTagsState = () => wrapper.find(EmptyTagsState); const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert); const routeId = 1; const breadCrumbState = { updateName: jest.fn(), }; const cleanTags = tagsMock.map(t => { const result = { ...t }; // eslint-disable-next-line no-underscore-dangle delete result.__typename; return result; }); const waitForApolloRequestRender = async () => { await waitForPromises(); await wrapper.vm.$nextTick(); }; const tagsArrayToSelectedTags = tags => tags.reduce((acc, c) => { acc[c.name] = true; return acc; }, {}); const mountComponent = ({ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), options, config = {}, } = {}) => { localVue.use(VueApollo); const requestHandlers = [ [getContainerRepositoryDetailsQuery, resolver], [deleteContainerRepositoryTagsMutation, mutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); wrapper = shallowMount(component, { localVue, apolloProvider, stubs: { DeleteModal, }, mocks: { $route: { params: { id: routeId, }, }, }, provide() { return { breadCrumbState, config, }; }, ...options, }); }; beforeEach(() => { jest.spyOn(Tracking, 'event'); }); afterEach(() => { wrapper.destroy(); wrapper = null; }); describe('when isLoading is true', () => { it('shows the loader', () => { mountComponent(); expect(findTagsLoader().exists()).toBe(true); }); it('does not show the list', () => { mountComponent(); expect(findTagsList().exists()).toBe(false); }); it('does not show pagination', () => { mountComponent(); expect(findPagination().exists()).toBe(false); }); }); describe('when the list of tags is empty', () => { const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock); it('has the empty state', async () => { mountComponent({ resolver }); await waitForApolloRequestRender(); expect(findEmptyTagsState().exists()).toBe(true); }); it('does not show the loader', async () => { mountComponent({ resolver }); await waitForApolloRequestRender(); expect(findTagsLoader().exists()).toBe(false); }); it('does not show the list', async () => { mountComponent({ resolver }); await waitForApolloRequestRender(); expect(findTagsList().exists()).toBe(false); }); }); describe('list', () => { it('exists', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findTagsList().exists()).toBe(true); }); it('has the correct props bound', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findTagsList().props()).toMatchObject({ isMobile: false, tags: cleanTags, }); }); describe('deleteEvent', () => { describe('single item', () => { let tagToBeDeleted; beforeEach(async () => { mountComponent(); await waitForApolloRequestRender(); [tagToBeDeleted] = cleanTags; findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true }); }); it('open the modal', async () => { expect(DeleteModal.methods.show).toHaveBeenCalled(); }); it('tracks a single delete event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { label: 'registry_tag_delete', }); }); }); describe('multiple items', () => { beforeEach(async () => { mountComponent(); await waitForApolloRequestRender(); findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(cleanTags)); }); it('open the modal', () => { expect(DeleteModal.methods.show).toHaveBeenCalled(); }); it('tracks a single delete event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { label: 'bulk_registry_tag_delete', }); }); }); }); }); describe('pagination', () => { it('exists', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findPagination().exists()).toBe(true); }); it('is hidden when there are no more pages', async () => { mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock) }); await waitForApolloRequestRender(); expect(findPagination().exists()).toBe(false); }); it('is wired to the correct pagination props', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findPagination().props()).toMatchObject({ hasNextPage: tagsPageInfo.hasNextPage, hasPreviousPage: tagsPageInfo.hasPreviousPage, }); }); it('fetch next page when user clicks next', async () => { const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()); mountComponent({ resolver }); await waitForApolloRequestRender(); findPagination().vm.$emit('next'); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ after: tagsPageInfo.endCursor }), ); }); it('fetch previous page when user clicks prev', async () => { const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()); mountComponent({ resolver }); await waitForApolloRequestRender(); findPagination().vm.$emit('prev'); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }), ); }); }); describe('modal', () => { it('exists', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findDeleteModal().exists()).toBe(true); }); describe('cancel event', () => { it('tracks cancel_delete', async () => { mountComponent(); await waitForApolloRequestRender(); findDeleteModal().vm.$emit('cancel'); expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { label: 'registry_tag_delete', }); }); }); describe('confirmDelete event', () => { let mutationResolver; beforeEach(() => { mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock); mountComponent({ mutationResolver }); return waitForApolloRequestRender(); }); describe('when one item is selected to be deleted', () => { it('calls apollo mutation with the right parameters', async () => { findTagsList().vm.$emit('delete', { [cleanTags[0].name]: true }); await wrapper.vm.$nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: [cleanTags[0].name] }), ); }); }); describe('when more than one item is selected to be deleted', () => { it('calls apollo mutation with the right parameters', async () => { findTagsList().vm.$emit('delete', { ...tagsArrayToSelectedTags(tagsMock) }); await wrapper.vm.$nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: tagsMock.map(t => t.name) }), ); }); }); }); }); describe('Header', () => { it('exists', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findDetailsHeader().exists()).toBe(true); }); it('has the correct props', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findDetailsHeader().props('image')).toMatchObject({ name: containerRepositoryMock.name, project: { visibility: containerRepositoryMock.project.visibility, }, }); }); }); describe('Delete Alert', () => { const config = { isAdmin: true, garbageCollectionHelpPagePath: 'baz', }; const deleteAlertType = 'success_tag'; it('exists', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findDeleteAlert().exists()).toBe(true); }); it('has the correct props', async () => { mountComponent({ options: { data: () => ({ deleteAlertType, }), }, config, }); await waitForApolloRequestRender(); expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType }); }); }); describe('Partial Cleanup Alert', () => { const config = { runCleanupPoliciesHelpPagePath: 'foo', cleanupPoliciesHelpPagePath: 'bar', }; describe('when expiration_policy_started is not null', () => { let resolver; beforeEach(() => { resolver = jest.fn().mockResolvedValue( graphQLImageDetailsMock({ expirationPolicyStartedAt: Date.now().toString(), }), ); }); it('exists', async () => { mountComponent({ resolver }); await waitForApolloRequestRender(); expect(findPartialCleanupAlert().exists()).toBe(true); }); it('has the correct props', async () => { mountComponent({ resolver, config }); await waitForApolloRequestRender(); expect(findPartialCleanupAlert().props()).toEqual({ ...config }); }); it('dismiss hides the component', async () => { mountComponent({ resolver }); await waitForApolloRequestRender(); expect(findPartialCleanupAlert().exists()).toBe(true); findPartialCleanupAlert().vm.$emit('dismiss'); await wrapper.vm.$nextTick(); expect(findPartialCleanupAlert().exists()).toBe(false); }); }); describe('when expiration_policy_started is null', () => { it('the component is hidden', async () => { mountComponent(); await waitForApolloRequestRender(); expect(findPartialCleanupAlert().exists()).toBe(false); }); }); }); describe('Breadcrumb connection', () => { it('when the details are fetched updates the name', async () => { mountComponent(); await waitForApolloRequestRender(); expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name); }); }); });