gitlab-org--gitlab-foss/spec/frontend/registry/explorer/pages/details_spec.js

598 lines
17 KiB
JavaScript

import { GlKeysetPagination } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import StatusAlert from '~/registry/explorer/components/details_page/status_alert.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
} from '~/registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import component from '~/registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
graphQLImageDetailsEmptyTagsMock,
graphQLDeleteImageRepositoryTagsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
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 findEmptyState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
const findStatusAlert = () => wrapper.find(StatusAlert);
const findDeleteImage = () => wrapper.find(DeleteImage);
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,
DeleteImage,
},
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 image does not exist', () => {
it('does not show the default ui', async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(findTagsLoader().exists()).toBe(false);
expect(findDetailsHeader().exists()).toBe(false);
expect(findTagsList().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
});
it('shows an empty state message', async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(findEmptyState().exists()).toBe(true);
});
});
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(findEmptyState().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()).toMatchObject({
metadataLoading: false,
image: {
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',
userCalloutsPath: 'call_out_path',
userCalloutId: 'call_out_id',
showUnfinishedTagCleanupCallout: true,
};
describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => {
let resolver;
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(
graphQLImageDetailsMock({
expirationPolicyCleanupStatus: UNFINISHED_STATUS,
}),
);
});
it('exists', async () => {
mountComponent({ resolver, config });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true);
});
it('has the correct props', async () => {
mountComponent({ resolver, config });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().props()).toEqual({
runCleanupPoliciesHelpPagePath: config.runCleanupPoliciesHelpPagePath,
cleanupPoliciesHelpPagePath: config.cleanupPoliciesHelpPagePath,
});
});
it('dismiss hides the component', async () => {
jest.spyOn(axios, 'post').mockReturnValue();
mountComponent({ resolver, config });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true);
findPartialCleanupAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, {
feature_name: config.userCalloutId,
});
expect(findPartialCleanupAlert().exists()).toBe(false);
});
it('is hidden if the callout is dismissed', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => {
it('the component is hidden', async () => {
mountComponent({ config });
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);
});
});
describe('when the image has a status different from null', () => {
const resolver = jest
.fn()
.mockResolvedValue(graphQLImageDetailsMock({ status: DELETE_SCHEDULED }));
it('disables all the actions', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findDetailsHeader().props('disabled')).toBe(true);
expect(findTagsList().props('disabled')).toBe(true);
});
it('shows a status alert', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findStatusAlert().exists()).toBe(true);
expect(findStatusAlert().props()).toMatchObject({
status: DELETE_SCHEDULED,
});
});
});
describe('delete the image', () => {
const mountComponentAndDeleteImage = async () => {
mountComponent();
await waitForApolloRequestRender();
findDetailsHeader().vm.$emit('delete');
await wrapper.vm.$nextTick();
};
it('on delete event it deletes the image', async () => {
await mountComponentAndDeleteImage();
findDeleteModal().vm.$emit('confirmDelete');
expect(findDeleteImage().emitted('start')).toEqual([[]]);
});
it('binds the correct props to the modal', async () => {
await mountComponentAndDeleteImage();
expect(findDeleteModal().props()).toMatchObject({
itemsToBeDeleted: [{ path: 'gitlab-org/gitlab-test/rails-12009' }],
deleteImage: true,
});
});
it('binds correctly to delete-image start and end events', async () => {
mountComponent();
findDeleteImage().vm.$emit('start');
await wrapper.vm.$nextTick();
expect(findTagsLoader().exists()).toBe(true);
findDeleteImage().vm.$emit('end');
await wrapper.vm.$nextTick();
expect(findTagsLoader().exists()).toBe(false);
});
it('binds correctly to delete-image error event', async () => {
mountComponent();
findDeleteImage().vm.$emit('error');
await wrapper.vm.$nextTick();
expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE);
});
});
});