Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/packages_and_registries/container_registry/explorer/pages')
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js521
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js597
3 files changed, 1142 insertions, 0 deletions
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
new file mode 100644
index 00000000000..adc9a64e5c9
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -0,0 +1,521 @@
+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 '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import DeleteAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
+import DetailsHeader from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
+import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
+import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
+import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+
+import {
+ UNFINISHED_STATUS,
+ DELETE_SCHEDULED,
+ ALERT_DANGER_IMAGE,
+ MISSING_OR_DELETED_IMAGE_BREADCRUMB,
+ ROOT_IMAGE_TEXT,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+
+import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
+import Tracking from '~/tracking';
+
+import {
+ graphQLImageDetailsMock,
+ graphQLDeleteImageRepositoryTagsMock,
+ containerRepositoryMock,
+ graphQLEmptyImageDetailsMock,
+ tagsMock,
+} 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 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);
+ });
+ });
+
+ 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('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,
+ });
+ });
+
+ describe('deleteEvent', () => {
+ describe('single item', () => {
+ let tagToBeDeleted;
+ beforeEach(async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ [tagToBeDeleted] = cleanTags;
+ findTagsList().vm.$emit('delete', [tagToBeDeleted]);
+ });
+
+ 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', 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('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]]);
+
+ 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', 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({
+ 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',
+ expirationPolicyHelpPagePath: '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.expirationPolicyHelpPagePath,
+ });
+ });
+
+ 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);
+ });
+
+ it(`when the image is missing set the breadcrumb to ${MISSING_OR_DELETED_IMAGE_BREADCRUMB}`, async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(MISSING_OR_DELETED_IMAGE_BREADCRUMB);
+ });
+
+ it(`when the image has no name set the breadcrumb to ${ROOT_IMAGE_TEXT}`, async () => {
+ mountComponent({
+ resolver: jest
+ .fn()
+ .mockResolvedValue(graphQLImageDetailsMock({ ...containerRepositoryMock, name: null })),
+ });
+
+ await waitForApolloRequestRender();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(ROOT_IMAGE_TEXT);
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
new file mode 100644
index 00000000000..5f4cb8969bc
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
@@ -0,0 +1,24 @@
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages_and_registries/container_registry/explorer/pages/index.vue';
+
+describe('List Page', () => {
+ let wrapper;
+
+ const findRouterView = () => wrapper.find({ ref: 'router-view' });
+
+ const mountComponent = () => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ RouterView: true,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('has a router view', () => {
+ expect(findRouterView().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
new file mode 100644
index 00000000000..051d1e2a169
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -0,0 +1,597 @@
+import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
+import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import CliCommands from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
+import ImageList from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ProjectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
+import RegistryHeader from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
+import {
+ DELETE_IMAGE_SUCCESS_MESSAGE,
+ DELETE_IMAGE_ERROR_MESSAGE,
+ SORT_FIELDS,
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
+import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue';
+import Tracking from '~/tracking';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+
+import { $toast } from 'jest/packages_and_registries/shared/mocks';
+import {
+ graphQLImageListMock,
+ graphQLImageDeleteMock,
+ deletedContainerRepository,
+ graphQLEmptyImageListMock,
+ graphQLEmptyGroupImageListMock,
+ pageInfo,
+ graphQLProjectImageRepositoriesDetailsMock,
+ dockerCommands,
+} from '../mock_data';
+import { GlModal, GlEmptyState } from '../stubs';
+
+const localVue = createLocalVue();
+
+describe('List Page', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const findCliCommands = () => wrapper.findComponent(CliCommands);
+ const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState);
+ const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState);
+ const findRegistryHeader = () => wrapper.findComponent(RegistryHeader);
+
+ const findDeleteAlert = () => wrapper.findComponent(GlAlert);
+ const findImageList = () => wrapper.findComponent(ImageList);
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
+ const findDeleteImage = () => wrapper.findComponent(DeleteImage);
+ const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
+
+ const waitForApolloRequestRender = async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ await nextTick();
+ };
+
+ const mountComponent = ({
+ mocks,
+ resolver = jest.fn().mockResolvedValue(graphQLImageListMock),
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
+ mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
+ config = { isGroupPage: false },
+ query = {},
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getContainerRepositoriesQuery, resolver],
+ [getContainerRepositoriesDetails, detailsResolver],
+ [deleteContainerRepositoryMutation, mutationResolver],
+ ];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ apolloProvider,
+ stubs: {
+ GlModal,
+ GlEmptyState,
+ GlSprintf,
+ RegistryHeader,
+ TitleArea,
+ DeleteImage,
+ },
+ mocks: {
+ $toast,
+ $route: {
+ name: 'foo',
+ query,
+ },
+ ...mocks,
+ },
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains registry header', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findRegistryHeader().exists()).toBe(true);
+ expect(findRegistryHeader().props()).toMatchObject({
+ imagesCount: 2,
+ metadataLoading: false,
+ });
+ });
+
+ describe.each([
+ { error: 'connectionError', errorName: 'connection error' },
+ { error: 'invalidPathError', errorName: 'invalid path error' },
+ ])('handling $errorName', ({ error }) => {
+ const config = {
+ containersErrorImage: 'foo',
+ helpPagePath: 'bar',
+ isGroupPage: false,
+ };
+ config[error] = true;
+
+ it('should show an empty state', () => {
+ mountComponent({ config });
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('empty state should have an svg-path', () => {
+ mountComponent({ config });
+
+ expect(findEmptyState().props('svgPath')).toBe(config.containersErrorImage);
+ });
+
+ it('empty state should have a description', () => {
+ mountComponent({ config });
+
+ expect(findEmptyState().props('title')).toContain('connection error');
+ });
+
+ it('should not show the loading or default state', () => {
+ mountComponent({ config });
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findImageList().exists()).toBe(false);
+ });
+ });
+
+ describe('isLoading is true', () => {
+ it('shows the skeleton loader', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('imagesList is not visible', () => {
+ mountComponent();
+
+ expect(findImageList().exists()).toBe(false);
+ });
+
+ it('cli commands is not visible', () => {
+ mountComponent();
+
+ expect(findCliCommands().exists()).toBe(false);
+ });
+
+ it('title has the metadataLoading props set to true', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(findRegistryHeader().props('metadataLoading')).toBe(true);
+ });
+ });
+
+ describe('list is empty', () => {
+ describe('project page', () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLEmptyImageListMock);
+
+ it('cli commands is not visible', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findCliCommands().exists()).toBe(false);
+ });
+
+ it('project empty state is visible', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findProjectEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('group page', () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);
+
+ const config = {
+ isGroupPage: true,
+ };
+
+ it('group empty state is visible', async () => {
+ mountComponent({ resolver, config });
+
+ await waitForApolloRequestRender();
+
+ expect(findGroupEmptyState().exists()).toBe(true);
+ });
+
+ it('cli commands is not visible', async () => {
+ mountComponent({ resolver, config });
+
+ await waitForApolloRequestRender();
+
+ expect(findCliCommands().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('list is not empty', () => {
+ describe('unfiltered state', () => {
+ it('quick start is visible', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findCliCommands().exists()).toBe(true);
+ });
+
+ it('list component is visible', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findImageList().exists()).toBe(true);
+ });
+
+ describe('additional metadata', () => {
+ it('is called on component load', async () => {
+ const detailsResolver = jest
+ .fn()
+ .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ detailsResolver });
+
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(detailsResolver).toHaveBeenCalled();
+ });
+
+ it('does not block the list ui to show', async () => {
+ const detailsResolver = jest.fn().mockRejectedValue();
+ mountComponent({ detailsResolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findImageList().exists()).toBe(true);
+ });
+
+ it('loading state is passed to list component', async () => {
+ // this is a promise that never resolves, to trick apollo to think that this request is still loading
+ const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {}));
+
+ mountComponent({ detailsResolver });
+ await waitForApolloRequestRender();
+
+ expect(findImageList().props('metadataLoading')).toBe(true);
+ });
+ });
+
+ describe('delete image', () => {
+ const selectImageForDeletion = async () => {
+ await waitForApolloRequestRender();
+
+ findImageList().vm.$emit('delete', deletedContainerRepository);
+ };
+
+ it('should call deleteItem when confirming deletion', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
+ mountComponent({ mutationResolver });
+
+ await selectImageForDeletion();
+
+ findDeleteModal().vm.$emit('primary');
+ await waitForApolloRequestRender();
+
+ expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
+
+ const updatedImage = findImageList()
+ .props('images')
+ .find((i) => i.id === deletedContainerRepository.id);
+
+ expect(updatedImage.status).toBe(deletedContainerRepository.status);
+ });
+
+ it('should show a success alert when delete request is successful', async () => {
+ mountComponent();
+
+ await selectImageForDeletion();
+
+ findDeleteImage().vm.$emit('success');
+ await nextTick();
+
+ const alert = findDeleteAlert();
+ expect(alert.exists()).toBe(true);
+ expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
+ DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
+ );
+ });
+
+ describe('when delete request fails it shows an alert', () => {
+ it('user recoverable error', async () => {
+ mountComponent();
+
+ await selectImageForDeletion();
+
+ findDeleteImage().vm.$emit('error');
+ await nextTick();
+
+ const alert = findDeleteAlert();
+ expect(alert.exists()).toBe(true);
+ expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
+ DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
+ );
+ });
+ });
+ });
+ });
+
+ describe('search and sorting', () => {
+ const doSearch = async () => {
+ await waitForApolloRequestRender();
+ findRegistrySearch().vm.$emit('filter:changed', [
+ { type: FILTERED_SEARCH_TERM, value: { data: 'centos6' } },
+ ]);
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ await nextTick();
+ };
+
+ it('has a search box element', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ const registrySearch = findRegistrySearch();
+ expect(registrySearch.exists()).toBe(true);
+ expect(registrySearch.props()).toMatchObject({
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
+ sortableFields: SORT_FIELDS,
+ tokens: [],
+ });
+ });
+
+ it('performs sorting', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
+ await nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
+ });
+
+ it('performs a search', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await doSearch();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ name: 'centos6' }));
+ });
+
+ it('when search result is empty displays an empty search message', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ const detailsResolver = jest
+ .fn()
+ .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ resolver, detailsResolver });
+
+ await waitForApolloRequestRender();
+
+ resolver.mockResolvedValue(graphQLEmptyImageListMock);
+ detailsResolver.mockResolvedValue(graphQLEmptyImageListMock);
+
+ await doSearch();
+
+ expect(findEmptySearchMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('pagination', () => {
+ it('prev-page event triggers a fetchMore request', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ const detailsResolver = jest
+ .fn()
+ .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ resolver, detailsResolver });
+
+ await waitForApolloRequestRender();
+
+ findImageList().vm.$emit('prev-page');
+ await nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ before: pageInfo.startCursor }),
+ );
+ expect(detailsResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ before: pageInfo.startCursor }),
+ );
+ });
+
+ it('next-page event triggers a fetchMore request', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ const detailsResolver = jest
+ .fn()
+ .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ resolver, detailsResolver });
+
+ await waitForApolloRequestRender();
+
+ findImageList().vm.$emit('next-page');
+ await nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: pageInfo.endCursor }),
+ );
+ expect(detailsResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: pageInfo.endCursor }),
+ );
+ });
+ });
+ });
+
+ describe('modal', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('exists', () => {
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+
+ it('contains a description with the path of the item to delete', async () => {
+ findImageList().vm.$emit('delete', { path: 'foo' });
+ await nextTick();
+ expect(findDeleteModal().html()).toContain('foo');
+ });
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const testTrackingCall = (action) => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
+ label: 'registry_repository_delete',
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ });
+
+ it('send an event when delete button is clicked', () => {
+ findImageList().vm.$emit('delete', {});
+
+ testTrackingCall('click_button');
+ });
+
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ testTrackingCall('cancel_delete');
+ });
+
+ it('send an event when the deletion starts', () => {
+ findDeleteImage().vm.$emit('start');
+ testTrackingCall('confirm_delete');
+ });
+ });
+
+ describe('url query string handling', () => {
+ const defaultQueryParams = {
+ search: [1, 2],
+ sort: 'asc',
+ orderBy: 'CREATED',
+ };
+ const queryChangePayload = 'foo';
+
+ it('query:updated event pushes the new query to the router', async () => {
+ const push = jest.fn();
+ mountComponent({ mocks: { $router: { push } } });
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('query:changed', queryChangePayload);
+
+ expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
+ });
+
+ it('graphql API call has the variables set from the URL', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ query: defaultQueryParams, resolver });
+
+ await nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 1,
+ sort: 'CREATED_ASC',
+ }),
+ );
+ });
+
+ it.each`
+ sort | orderBy | search | payload
+ ${'ASC'} | ${undefined} | ${undefined} | ${{ sort: 'UPDATED_ASC' }}
+ ${undefined} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_DESC' }}
+ ${'ASC'} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_ASC' }}
+ ${undefined} | ${undefined} | ${undefined} | ${{}}
+ ${undefined} | ${undefined} | ${['one']} | ${{ name: 'one' }}
+ ${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
+ ${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
+ ${'ASC'} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
+ `(
+ 'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
+ async ({ sort, orderBy, search, payload }) => {
+ const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
+ mountComponent({ query: { sort, orderBy, search }, resolver });
+
+ await nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
+ },
+ );
+ });
+
+ describe('cleanup is on alert', () => {
+ it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
+ mountComponent({
+ config: {
+ showCleanupPolicyOnAlert: true,
+ projectPath: 'foo',
+ isGroupPage: false,
+ cleanupPoliciesSettingsPath: 'bar',
+ },
+ });
+
+ await waitForApolloRequestRender();
+
+ expect(findCleanupAlert().exists()).toBe(true);
+ expect(findCleanupAlert().props()).toMatchObject({
+ projectPath: 'foo',
+ cleanupPoliciesSettingsPath: 'bar',
+ });
+ });
+
+ it('is hidden when showCleanupPolicyOnAlert is false', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findCleanupAlert().exists()).toBe(false);
+ });
+ });
+});