diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 13:37:47 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 13:37:47 +0000 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /spec/frontend/packages_and_registries | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) | |
download | gitlab-ce-aee0a117a889461ce8ced6fcf73207fe017f1d99.tar.gz |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'spec/frontend/packages_and_registries')
53 files changed, 2521 insertions, 327 deletions
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js index 9a42c82d7e0..56f12e2f0bb 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js @@ -1,18 +1,16 @@ -import { GlButton, GlKeysetPagination } 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 { stripTypenames } from 'helpers/graphql_helpers'; import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue'; import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue'; import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue'; import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue'; -import { - TAGS_LIST_TITLE, - REMOVE_TAGS_BUTTON_TITLE, -} from '~/packages_and_registries/container_registry/explorer/constants/index'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql'; +import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/container_registry/explorer/constants/index'; import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data'; const localVue = createLocalVue(); @@ -20,25 +18,20 @@ const localVue = createLocalVue(); describe('Tags List', () => { let wrapper; let apolloProvider; + let resolver; const tags = [...tagsMock]; - const readOnlyTags = tags.map((t) => ({ ...t, canDelete: false })); - const findTagsListRow = () => wrapper.findAll(TagsListRow); - const findDeleteButton = () => wrapper.find(GlButton); - const findListTitle = () => wrapper.find('[data-testid="list-title"]'); - const findPagination = () => wrapper.find(GlKeysetPagination); - const findEmptyState = () => wrapper.find(EmptyTagsState); - const findTagsLoader = () => wrapper.find(TagsLoader); + const findTagsListRow = () => wrapper.findAllComponents(TagsListRow); + const findRegistryList = () => wrapper.findComponent(RegistryList); + const findEmptyState = () => wrapper.findComponent(EmptyTagsState); + const findTagsLoader = () => wrapper.findComponent(TagsLoader); const waitForApolloRequestRender = async () => { await waitForPromises(); await nextTick(); }; - const mountComponent = ({ - propsData = { isMobile: false, id: 1 }, - resolver = jest.fn().mockResolvedValue(imageTagsMock()), - } = {}) => { + const mountComponent = ({ propsData = { isMobile: false, id: 1 } } = {}) => { localVue.use(VueApollo); const requestHandlers = [[getContainerRepositoryTagsQuery, resolver]]; @@ -48,6 +41,7 @@ describe('Tags List', () => { localVue, apolloProvider, propsData, + stubs: { RegistryList }, provide() { return { config: {}, @@ -56,99 +50,58 @@ describe('Tags List', () => { }); }; + beforeEach(() => { + resolver = jest.fn().mockResolvedValue(imageTagsMock()); + }); + afterEach(() => { wrapper.destroy(); - wrapper = null; }); - describe('List title', () => { - it('exists', async () => { + describe('registry list', () => { + beforeEach(() => { mountComponent(); - await waitForApolloRequestRender(); - - expect(findListTitle().exists()).toBe(true); + return waitForApolloRequestRender(); }); - it('has the correct text', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findListTitle().text()).toBe(TAGS_LIST_TITLE); + it('binds the correct props', () => { + expect(findRegistryList().props()).toMatchObject({ + title: '2 tags', + pagination: stripTypenames(tagsPageInfo), + items: stripTypenames(tags), + idProperty: 'name', + }); }); - }); - describe('delete button', () => { - it.each` - inputTags | isMobile | isVisible - ${tags} | ${false} | ${true} - ${tags} | ${true} | ${false} - ${readOnlyTags} | ${false} | ${false} - ${readOnlyTags} | ${true} | ${false} - `( - 'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile', - async ({ inputTags, isMobile, isVisible }) => { - mountComponent({ - propsData: { tags: inputTags, isMobile, id: 1 }, - resolver: jest.fn().mockResolvedValue(imageTagsMock(inputTags)), + describe('events', () => { + it('prev-page fetch the previous page', () => { + findRegistryList().vm.$emit('prev-page'); + + expect(resolver).toHaveBeenCalledWith({ + first: null, + before: tagsPageInfo.startCursor, + last: GRAPHQL_PAGE_SIZE, + id: '1', }); - - await waitForApolloRequestRender(); - - expect(findDeleteButton().exists()).toBe(isVisible); - }, - ); - - it('has the correct text', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findDeleteButton().text()).toBe(REMOVE_TAGS_BUTTON_TITLE); - }); - - it('has the correct props', async () => { - mountComponent(); - await waitForApolloRequestRender(); - - expect(findDeleteButton().attributes()).toMatchObject({ - category: 'secondary', - variant: 'danger', }); - }); - - it.each` - disabled | doSelect | buttonDisabled - ${true} | ${false} | ${'true'} - ${true} | ${true} | ${'true'} - ${false} | ${false} | ${'true'} - ${false} | ${true} | ${undefined} - `( - 'is $buttonDisabled that the button is disabled when the component disabled state is $disabled and is $doSelect that the user selected a tag', - async ({ disabled, buttonDisabled, doSelect }) => { - mountComponent({ propsData: { tags, disabled, isMobile: false, id: 1 } }); - - await waitForApolloRequestRender(); - - if (doSelect) { - findTagsListRow().at(0).vm.$emit('select'); - await nextTick(); - } - expect(findDeleteButton().attributes('disabled')).toBe(buttonDisabled); - }, - ); + it('next-page fetch the previous page', () => { + findRegistryList().vm.$emit('next-page'); - it('click event emits a deleted event with selected items', async () => { - mountComponent(); - - await waitForApolloRequestRender(); + expect(resolver).toHaveBeenCalledWith({ + after: tagsPageInfo.endCursor, + first: GRAPHQL_PAGE_SIZE, + id: '1', + }); + }); - findTagsListRow().at(0).vm.$emit('select'); - findDeleteButton().vm.$emit('click'); + it('emits a delete event when list emits delete', () => { + const eventPayload = 'foo'; + findRegistryList().vm.$emit('delete', eventPayload); - expect(wrapper.emitted('delete')[0][0][0].name).toBe(tags[0].name); + expect(wrapper.emitted('delete')).toEqual([[eventPayload]]); + }); }); }); @@ -199,10 +152,12 @@ describe('Tags List', () => { }); describe('when the list of tags is empty', () => { - const resolver = jest.fn().mockResolvedValue(imageTagsMock([])); + beforeEach(() => { + resolver = jest.fn().mockResolvedValue(imageTagsMock([])); + }); it('has the empty state', async () => { - mountComponent({ resolver }); + mountComponent(); await waitForApolloRequestRender(); @@ -210,7 +165,7 @@ describe('Tags List', () => { }); it('does not show the loader', async () => { - mountComponent({ resolver }); + mountComponent(); await waitForApolloRequestRender(); @@ -218,76 +173,13 @@ describe('Tags List', () => { }); it('does not show the list', async () => { - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - expect(findTagsListRow().exists()).toBe(false); - expect(findListTitle().exists()).toBe(false); - }); - }); - - describe('pagination', () => { - it('exists', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findPagination().exists()).toBe(true); - }); - - it('is hidden when loading', () => { mountComponent(); - expect(findPagination().exists()).toBe(false); - }); - - it('is hidden when there are no more pages', async () => { - mountComponent({ resolver: jest.fn().mockResolvedValue(imageTagsMock([])) }); - 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(imageTagsMock()); - 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(imageTagsMock()); - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - findPagination().vm.$emit('prev'); - - expect(resolver).toHaveBeenCalledWith( - expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }), - ); + expect(findRegistryList().exists()).toBe(false); }); }); - describe('loading state', () => { it.each` isImageLoading | queryExecuting | loadingVisible @@ -306,8 +198,6 @@ describe('Tags List', () => { expect(findTagsLoader().exists()).toBe(loadingVisible); expect(findTagsListRow().exists()).toBe(!loadingVisible); - expect(findListTitle().exists()).toBe(!loadingVisible); - expect(findPagination().exists()).toBe(!loadingVisible); }, ); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap index 46b07b4c2d6..4b52e84d1a6 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap @@ -36,6 +36,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` <gl-form-input-group-stub class="gl-mb-4" + inputclass="" predefinedoptions="[object Object]" value="" > @@ -57,6 +58,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` <gl-form-input-group-stub class="gl-mb-4" + inputclass="" predefinedoptions="[object Object]" value="" > @@ -69,6 +71,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` </gl-form-input-group-stub> <gl-form-input-group-stub + inputclass="" predefinedoptions="[object Object]" value="" > diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js index 6a835a28807..16625d913a5 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js @@ -37,6 +37,7 @@ export const graphQLImageListMock = { data: { project: { __typename: 'Project', + id: '1', containerRepositoriesCount: 2, containerRepositories: { __typename: 'ContainerRepositoryConnection', @@ -51,6 +52,7 @@ export const graphQLEmptyImageListMock = { data: { project: { __typename: 'Project', + id: '1', containerRepositoriesCount: 2, containerRepositories: { __typename: 'ContainerRepositoryConnection', @@ -65,6 +67,7 @@ export const graphQLEmptyGroupImageListMock = { data: { group: { __typename: 'Group', + id: '1', containerRepositoriesCount: 2, containerRepositories: { __typename: 'ContainerRepositoryConnection', @@ -120,6 +123,7 @@ export const containerRepositoryMock = { project: { visibility: 'public', path: 'gitlab-test', + id: '1', containerExpirationPolicy: { enabled: false, nextRunAt: '2020-11-27T08:59:27Z', @@ -167,6 +171,7 @@ export const imageTagsMock = (nodes = tagsMock) => ({ data: { containerRepository: { id: containerRepositoryMock.id, + tagsCount: nodes.length, tags: { nodes, pageInfo: { ...tagsPageInfo }, @@ -191,7 +196,7 @@ export const graphQLImageDetailsMock = (override) => ({ data: { containerRepository: { ...containerRepositoryMock, - + tagsCount: tagsMock.length, tags: { nodes: tagsMock, pageInfo: { ...tagsPageInfo }, @@ -242,6 +247,7 @@ export const dockerCommands = { export const graphQLProjectImageRepositoriesDetailsMock = { data: { project: { + id: '1', containerRepositories: { nodes: [ { 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 index adc9a64e5c9..9b821ba8ef3 100644 --- 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 @@ -1,6 +1,7 @@ import { GlKeysetPagination } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +import { nextTick } from 'vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; @@ -22,6 +23,7 @@ import { } 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 getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql'; import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue'; import Tracking from '~/tracking'; @@ -32,6 +34,7 @@ import { containerRepositoryMock, graphQLEmptyImageDetailsMock, tagsMock, + imageTagsMock, } from '../mock_data'; import { DeleteModal } from '../stubs'; @@ -67,12 +70,13 @@ describe('Details Page', () => { const waitForApolloRequestRender = async () => { await waitForPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); }; const mountComponent = ({ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)), options, config = {}, } = {}) => { @@ -81,6 +85,7 @@ describe('Details Page', () => { const requestHandlers = [ [getContainerRepositoryDetailsQuery, resolver], [deleteContainerRepositoryTagsMutation, mutationResolver], + [getContainerRepositoryTagsQuery, tagsResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -242,38 +247,49 @@ describe('Details Page', () => { describe('confirmDelete event', () => { let mutationResolver; + let tagsResolver; beforeEach(() => { mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock); - mountComponent({ mutationResolver }); + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)); + mountComponent({ mutationResolver, tagsResolver }); return waitForApolloRequestRender(); }); + describe('when one item is selected to be deleted', () => { - it('calls apollo mutation with the right parameters', async () => { + it('calls apollo mutation with the right parameters and refetches the tags list query', async () => { findTagsList().vm.$emit('delete', [cleanTags[0]]); - await wrapper.vm.$nextTick(); + await nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: [cleanTags[0].name] }), ); + + await waitForPromises(); + + expect(tagsResolver).toHaveBeenCalled(); }); }); describe('when more than one item is selected to be deleted', () => { - it('calls apollo mutation with the right parameters', async () => { + it('calls apollo mutation with the right parameters and refetches the tags list query', async () => { findTagsList().vm.$emit('delete', tagsMock); - await wrapper.vm.$nextTick(); + await nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }), ); + + await waitForPromises(); + + expect(tagsResolver).toHaveBeenCalled(); }); }); }); @@ -382,7 +398,7 @@ describe('Details Page', () => { findPartialCleanupAlert().vm.$emit('dismiss'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, { feature_name: config.userCalloutId, @@ -472,7 +488,7 @@ describe('Details Page', () => { await waitForApolloRequestRender(); findDetailsHeader().vm.$emit('delete'); - await wrapper.vm.$nextTick(); + await nextTick(); }; it('on delete event it deletes the image', async () => { @@ -497,13 +513,13 @@ describe('Details Page', () => { findDeleteImage().vm.$emit('start'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findTagsLoader().exists()).toBe(true); findDeleteImage().vm.$emit('end'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findTagsLoader().exists()).toBe(false); }); @@ -513,7 +529,7 @@ describe('Details Page', () => { findDeleteImage().vm.$emit('error'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js index 625f00a8666..44a7186904d 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -54,7 +54,6 @@ describe('DependencyProxyApp', () => { } const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available'); - const findProxyDisabledAlert = () => wrapper.findByTestId('proxy-disabled'); const findClipBoardButton = () => wrapper.findComponent(ClipboardButton); const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup); @@ -219,28 +218,6 @@ describe('DependencyProxyApp', () => { }); }); }); - - describe('when the dependency proxy is disabled', () => { - beforeEach(() => { - resolver = jest - .fn() - .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })); - createComponent(); - return waitForPromises(); - }); - - it('does not show the main area', () => { - expect(findMainArea().exists()).toBe(false); - }); - - it('does not show the loader', () => { - expect(findSkeletonLoader().exists()).toBe(false); - }); - - it('shows a proxy disabled alert', () => { - expect(findProxyDisabledAlert().text()).toBe(DependencyProxyApp.i18n.proxyDisabledText); - }); - }); }); }); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js index 8bad22b5287..2aa427bc6af 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js @@ -8,8 +8,8 @@ export const proxyData = () => ({ export const proxySettings = (extend = {}) => ({ enabled: true, ...extend }); export const proxyManifests = () => [ - { createdAt: '2021-09-22T09:45:28Z', imageName: 'alpine:latest' }, - { createdAt: '2021-09-21T09:45:28Z', imageName: 'alpine:stable' }, + { id: 'proxy-1', createdAt: '2021-09-22T09:45:28Z', imageName: 'alpine:latest' }, + { id: 'proxy-2', createdAt: '2021-09-21T09:45:28Z', imageName: 'alpine:stable' }, ]; export const pagination = (extend) => ({ @@ -26,6 +26,7 @@ export const proxyDetailsQuery = ({ extendSettings = {}, extend } = {}) => ({ group: { ...proxyData(), __typename: 'Group', + id: '1', dependencyProxySetting: { ...proxySettings(extendSettings), __typename: 'DependencyProxySetting', diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js index c7c10cef504..2868af84181 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js @@ -9,15 +9,15 @@ import PackagesApp from '~/packages_and_registries/infrastructure_registry/detai import PackageFiles from '~/packages_and_registries/infrastructure_registry/details/components/package_files.vue'; import PackageHistory from '~/packages_and_registries/infrastructure_registry/details/components/package_history.vue'; import * as getters from '~/packages_and_registries/infrastructure_registry/details/store/getters'; -import PackageListRow from '~/packages/shared/components/package_list_row.vue'; -import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; -import { TrackingActions } from '~/packages/shared/constants'; -import * as SharedUtils from '~/packages/shared/utils'; +import PackageListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; +import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants'; +import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; import TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue'; import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue'; import Tracking from '~/tracking'; -import { mavenPackage, mavenFiles, npmPackage } from 'jest/packages/mock_data'; +import { mavenPackage, mavenFiles, npmPackage } from '../../mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -232,87 +232,78 @@ describe('PackagesApp', () => { describe('tracking', () => { let eventSpy; - let utilSpy; - const category = 'foo'; beforeEach(() => { eventSpy = jest.spyOn(Tracking, 'event'); - utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category); }); - it('tracking category calls packageTypeToTrackCategory', () => { - createComponent({ packageEntity: npmPackage }); - expect(wrapper.vm.tracking.category).toBe(category); - expect(utilSpy).toHaveBeenCalledWith('npm'); - }); - - it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => { + it(`delete button on delete modal call event with ${TRACKING_ACTIONS.DELETE_PACKAGE}`, () => { createComponent({ packageEntity: npmPackage }); findDeleteModal().vm.$emit('primary'); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.DELETE_PACKAGE, + TRACK_CATEGORY, + TRACKING_ACTIONS.DELETE_PACKAGE, expect.any(Object), ); }); - it(`canceling a package deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE}`, () => { + it(`canceling a package deletion tracks ${TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE}`, () => { createComponent({ packageEntity: npmPackage }); findDeleteModal().vm.$emit('canceled'); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.CANCEL_DELETE_PACKAGE, + TRACK_CATEGORY, + TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE, expect.any(Object), ); }); - it(`request a file deletion tracks ${TrackingActions.REQUEST_DELETE_PACKAGE_FILE}`, () => { + it(`request a file deletion tracks ${TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE_FILE}`, () => { createComponent({ packageEntity: npmPackage }); findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + TRACK_CATEGORY, + TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE_FILE, expect.any(Object), ); }); - it(`confirming a file deletion tracks ${TrackingActions.DELETE_PACKAGE_FILE}`, () => { + it(`confirming a file deletion tracks ${TRACKING_ACTIONS.DELETE_PACKAGE_FILE}`, () => { createComponent({ packageEntity: npmPackage }); findPackageFiles().vm.$emit('delete-file', npmPackage); findDeleteFileModal().vm.$emit('primary'); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + TRACK_CATEGORY, + TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE_FILE, expect.any(Object), ); }); - it(`canceling a file deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE_FILE}`, () => { + it(`canceling a file deletion tracks ${TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE_FILE}`, () => { createComponent({ packageEntity: npmPackage }); findPackageFiles().vm.$emit('delete-file', npmPackage); findDeleteFileModal().vm.$emit('canceled'); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.CANCEL_DELETE_PACKAGE_FILE, + TRACK_CATEGORY, + TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE_FILE, expect.any(Object), ); }); - it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => { + it(`file download link call event with ${TRACKING_ACTIONS.PULL_PACKAGE}`, () => { createComponent({ packageEntity: npmPackage }); findPackageFiles().vm.$emit('download-file'); expect(eventSpy).toHaveBeenCalledWith( - category, - TrackingActions.PULL_PACKAGE, + TRACK_CATEGORY, + TRACKING_ACTIONS.PULL_PACKAGE, expect.any(Object), ); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js index a012ec4ab05..24bd80ba80c 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js @@ -1,8 +1,8 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { terraformModule, mavenFiles, npmPackage } from 'jest/packages/mock_data'; import component from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { terraformModule, mavenFiles, npmPackage } from '../../mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js index 0c5aa30223b..6b6c33b7561 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js @@ -6,7 +6,7 @@ import component from '~/packages_and_registries/infrastructure_registry/details import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { npmFiles, mavenFiles } from 'jest/packages/mock_data'; +import { npmFiles, mavenFiles } from '../../mock_data'; describe('Package Files', () => { let wrapper; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js index 4987af9f5b0..f10f05f4a0d 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js @@ -6,7 +6,7 @@ import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/consta import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { mavenPackage, mockPipelineInfo } from 'jest/packages/mock_data'; +import { mavenPackage, mockPipelineInfo } from '../../mock_data'; describe('Package History', () => { let wrapper; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js index c26784a4b75..6ff4a4c51ef 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js @@ -1,8 +1,8 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { terraformModule as packageEntity } from 'jest/packages/mock_data'; import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue'; import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue'; +import { terraformModule as packageEntity } from '../../mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js index 61fa69c2f7a..b9383d6c38c 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js @@ -12,8 +12,8 @@ import { DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, -} from '~/packages/shared/constants'; -import { npmPackage as packageEntity } from '../../../../../packages/mock_data'; +} from '~/packages_and_registries/shared/constants'; +import { npmPackage as packageEntity } from '../../mock_data'; jest.mock('~/flash.js'); jest.mock('~/api.js'); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js index 8740691a8ee..b14aaa93e1f 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js @@ -3,7 +3,7 @@ import { npmPackage, mockPipelineInfo, mavenPackage as packageWithoutBuildInfo, -} from 'jest/packages/mock_data'; +} from '../../mock_data'; describe('Getters PackageDetails Store', () => { let state; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js index 6efefea4a14..0f0c84af7da 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js @@ -1,6 +1,6 @@ import * as types from '~/packages_and_registries/infrastructure_registry/details/store/mutation_types'; import mutations from '~/packages_and_registries/infrastructure_registry/details/store/mutations'; -import { npmPackage as packageEntity } from 'jest/packages/mock_data'; +import { npmPackage as packageEntity } from '../../mock_data'; describe('Mutations package details Store', () => { let mockState; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap new file mode 100644 index 00000000000..99a7b8e427a --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`packages_list_app renders 1`] = ` +<div> + <infrastructure-title-stub + helpurl="foo" + /> + + <infrastructure-search-stub /> + + <div> + <section + class="row empty-state text-center" + > + <div + class="col-12" + > + <div + class="svg-250 svg-content" + > + <img + alt="" + class="gl-max-w-full" + role="img" + src="helpSvg" + /> + </div> + </div> + + <div + class="col-12" + > + <div + class="text-content gl-mx-auto gl-my-0 gl-p-5" + > + <h1 + class="gl-font-size-h-display gl-line-height-36 h4" + > + + There are no packages yet + + </h1> + + <p + class="gl-mt-3" + > + Learn how to + <b-link-stub + class="gl-link" + event="click" + href="helpUrl" + routertag="a" + target="_blank" + > + publish and share your packages + </b-link-stub> + with GitLab. + </p> + + <div + class="gl-display-flex gl-flex-wrap gl-justify-content-center" + > + <!----> + + <!----> + </div> + </div> + </div> + </section> + </div> +</div> +`; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js index 119b678cc37..b519ab00d06 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_search_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js @@ -1,6 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'; +import component from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue'; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js index db6e175b054..b0e586f189a 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_title_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'; +import component from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js new file mode 100644 index 00000000000..cad75d2a858 --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js @@ -0,0 +1,239 @@ +import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import createFlash from '~/flash'; +import * as commonUtils from '~/lib/utils/common_utils'; +import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue'; +import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants'; +import { + SHOW_DELETE_SUCCESS_ALERT, + FILTERED_SEARCH_TERM, +} from '~/packages_and_registries/shared/constants'; + +import * as packageUtils from '~/packages_and_registries/shared/utils'; +import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue'; + +jest.mock('~/lib/utils/common_utils'); +jest.mock('~/flash'); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('packages_list_app', () => { + let wrapper; + let store; + + const PackageList = { + name: 'package-list', + template: '<div><slot name="empty-state"></slot></div>', + }; + const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' }; + + const emptyListHelpUrl = 'helpUrl'; + const findEmptyState = () => wrapper.find(GlEmptyState); + const findListComponent = () => wrapper.find(PackageList); + const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch); + + const createStore = (filter = []) => { + store = new Vuex.Store({ + state: { + isLoading: false, + config: { + resourceId: 'project_id', + emptyListIllustration: 'helpSvg', + emptyListHelpUrl, + packageHelpUrl: 'foo', + }, + filter, + }, + }); + store.dispatch = jest.fn(); + }; + + const mountComponent = (provide) => { + wrapper = shallowMount(PackageListApp, { + localVue, + store, + stubs: { + GlEmptyState, + GlLoadingIcon, + PackageList, + GlSprintf, + GlLink, + }, + provide, + }); + }; + + beforeEach(() => { + createStore(); + jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({}); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + mountComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('call requestPackagesList on page:changed', () => { + mountComponent(); + store.dispatch.mockClear(); + + const list = findListComponent(); + list.vm.$emit('page:changed', 1); + expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 }); + }); + + it('call requestDeletePackage on package:delete', () => { + mountComponent(); + + const list = findListComponent(); + list.vm.$emit('package:delete', 'foo'); + expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo'); + }); + + it('does call requestPackagesList only one time on render', () => { + mountComponent(); + + expect(store.dispatch).toHaveBeenCalledTimes(3); + expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object)); + expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array)); + expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList'); + }); + + describe('url query string handling', () => { + const defaultQueryParamsMock = { + search: [1, 2], + type: 'npm', + sort: 'asc', + orderBy: 'created', + }; + + it('calls setSorting with the query string based sorting', () => { + jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock); + + mountComponent(); + + expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { + orderBy: defaultQueryParamsMock.orderBy, + sort: defaultQueryParamsMock.sort, + }); + }); + + it('calls setFilter with the query string based filters', () => { + jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock); + + mountComponent(); + + expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [ + { type: 'type', value: { data: defaultQueryParamsMock.type } }, + { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } }, + { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } }, + ]); + }); + + it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => { + jest + .spyOn(packageUtils, 'extractFilterAndSorting') + .mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } }); + + mountComponent(); + + expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' }); + expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']); + }); + }); + + describe('empty state', () => { + it('generate the correct empty list link', () => { + mountComponent(); + + const link = findListComponent().find(GlLink); + + expect(link.attributes('href')).toBe(emptyListHelpUrl); + expect(link.text()).toBe('publish and share your packages'); + }); + + it('includes the right content on the default tab', () => { + mountComponent(); + + const heading = findEmptyState().find('h1'); + + expect(heading.text()).toBe('There are no packages yet'); + }); + }); + + describe('filter without results', () => { + beforeEach(() => { + createStore([{ type: 'something' }]); + mountComponent(); + }); + + it('should show specific empty message', () => { + expect(findEmptyState().text()).toContain('Sorry, your filter produced no results'); + expect(findEmptyState().text()).toContain( + 'To widen your search, change or remove the filters above', + ); + }); + }); + + describe('Search', () => { + it('exists', () => { + mountComponent(); + + expect(findInfrastructureSearch().exists()).toBe(true); + }); + + it('on update fetches data from the store', () => { + mountComponent(); + store.dispatch.mockClear(); + + findInfrastructureSearch().vm.$emit('update'); + + expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList'); + }); + }); + + describe('delete alert handling', () => { + const originalLocation = window.location.href; + const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`; + + beforeEach(() => { + createStore(); + jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {}); + setWindowLocation(search); + }); + + afterEach(() => { + setWindowLocation(originalLocation); + }); + + it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => { + mountComponent(); + + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_SUCCESS_MESSAGE, + type: 'notice', + }); + }); + + it('calls historyReplaceState with a clean url', () => { + mountComponent(); + + expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation); + }); + + it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => { + setWindowLocation('?'); + mountComponent(); + + expect(createFlash).not.toHaveBeenCalled(); + expect(commonUtils.historyReplaceState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js new file mode 100644 index 00000000000..2fb76b98925 --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js @@ -0,0 +1,209 @@ +import { GlTable, GlPagination, GlModal } from '@gitlab/ui'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { last } from 'lodash'; +import Vuex from 'vuex'; +import stubChildren from 'helpers/stub_children'; +import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue'; +import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; +import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants'; +import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; +import Tracking from '~/tracking'; +import { packageList } from '../../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('packages_list', () => { + let wrapper; + let store; + + const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' }; + + const findPackagesListLoader = () => wrapper.find(PackagesListLoader); + const findPackageListPagination = () => wrapper.find(GlPagination); + const findPackageListDeleteModal = () => wrapper.find(GlModal); + const findEmptySlot = () => wrapper.find(EmptySlotStub); + const findPackagesListRow = () => wrapper.find(PackagesListRow); + + const createStore = (isGroupPage, packages, isLoading) => { + const state = { + isLoading, + packages, + pagination: { + perPage: 1, + total: 1, + page: 1, + }, + config: { + isGroupPage, + }, + sorting: { + orderBy: 'version', + sort: 'desc', + }, + }; + store = new Vuex.Store({ + state, + getters: { + getList: () => packages, + }, + }); + store.dispatch = jest.fn(); + }; + + const mountComponent = ({ + isGroupPage = false, + packages = packageList, + isLoading = false, + ...options + } = {}) => { + createStore(isGroupPage, packages, isLoading); + + wrapper = mount(PackagesList, { + localVue, + store, + stubs: { + ...stubChildren(PackagesList), + GlTable, + GlModal, + }, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when is loading', () => { + beforeEach(() => { + mountComponent({ + packages: [], + isLoading: true, + }); + }); + + it('shows skeleton loader when loading', () => { + expect(findPackagesListLoader().exists()).toBe(true); + }); + }); + + describe('when is not loading', () => { + beforeEach(() => { + mountComponent(); + }); + + it('does not show skeleton loader when not loading', () => { + expect(findPackagesListLoader().exists()).toBe(false); + }); + }); + + describe('layout', () => { + beforeEach(() => { + mountComponent(); + }); + + it('contains a pagination component', () => { + const sorting = findPackageListPagination(); + expect(sorting.exists()).toBe(true); + }); + + it('contains a modal component', () => { + const sorting = findPackageListDeleteModal(); + expect(sorting.exists()).toBe(true); + }); + }); + + describe('when the user can destroy the package', () => { + beforeEach(() => { + mountComponent(); + }); + + it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => { + const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show'); + const item = last(wrapper.vm.list); + + findPackagesListRow().vm.$emit('packageToDelete', item); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.itemToBeDeleted).toEqual(item); + expect(mockModalShow).toHaveBeenCalled(); + }); + }); + + it('deleteItemConfirmation resets itemToBeDeleted', () => { + wrapper.setData({ itemToBeDeleted: 1 }); + wrapper.vm.deleteItemConfirmation(); + expect(wrapper.vm.itemToBeDeleted).toEqual(null); + }); + + it('deleteItemConfirmation emit package:delete', () => { + const itemToBeDeleted = { id: 2 }; + wrapper.setData({ itemToBeDeleted }); + wrapper.vm.deleteItemConfirmation(); + return wrapper.vm.$nextTick(() => { + expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]); + }); + }); + + it('deleteItemCanceled resets itemToBeDeleted', () => { + wrapper.setData({ itemToBeDeleted: 1 }); + wrapper.vm.deleteItemCanceled(); + expect(wrapper.vm.itemToBeDeleted).toEqual(null); + }); + }); + + describe('when the list is empty', () => { + beforeEach(() => { + mountComponent({ + packages: [], + slots: { + 'empty-state': EmptySlotStub, + }, + }); + }); + + it('show the empty slot', () => { + const emptySlot = findEmptySlot(); + expect(emptySlot.exists()).toBe(true); + }); + }); + + describe('pagination component', () => { + let pagination; + let modelEvent; + + beforeEach(() => { + mountComponent(); + pagination = findPackageListPagination(); + // retrieve the event used by v-model, a more sturdy approach than hardcoding it + modelEvent = pagination.vm.$options.model.event; + }); + + it('emits page:changed events when the page changes', () => { + pagination.vm.$emit(modelEvent, 2); + expect(wrapper.emitted('page:changed')).toEqual([[2]]); + }); + }); + + describe('tracking', () => { + let eventSpy; + + beforeEach(() => { + mountComponent(); + eventSpy = jest.spyOn(Tracking, 'event'); + wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } }); + }); + + it('deleteItemConfirmation calls event', () => { + wrapper.vm.deleteItemConfirmation(); + expect(eventSpy).toHaveBeenCalledWith( + TRACK_CATEGORY, + TRACKING_ACTIONS.DELETE_PACKAGE, + expect.any(Object), + ); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js new file mode 100644 index 00000000000..3fbfe1060dc --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js @@ -0,0 +1,277 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants'; +import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions'; +import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types'; +import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants'; + +jest.mock('~/flash.js'); +jest.mock('~/api.js'); + +describe('Actions Package list store', () => { + const headers = 'bar'; + let mock; + + beforeEach(() => { + Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo', headers }); + Api.groupPackages = jest.fn().mockResolvedValue({ data: 'baz', headers }); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestPackagesList', () => { + const sorting = { + sort: 'asc', + orderBy: 'version', + }; + + const filter = []; + it('should fetch the project packages list when isGroupPage is false', (done) => { + testAction( + actions.requestPackagesList, + undefined, + { config: { isGroupPage: false, resourceId: 1 }, sorting, filter }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(Api.projectPackages).toHaveBeenCalledWith(1, { + params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy }, + }); + done(); + }, + ); + }); + + it('should fetch the group packages list when isGroupPage is true', (done) => { + testAction( + actions.requestPackagesList, + undefined, + { config: { isGroupPage: true, resourceId: 2 }, sorting, filter }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'receivePackagesListSuccess', payload: { data: 'baz', headers } }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(Api.groupPackages).toHaveBeenCalledWith(2, { + params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy }, + }); + done(); + }, + ); + }); + + it('should fetch packages of a certain type when a filter with a type is present', (done) => { + const packageType = 'maven'; + + testAction( + actions.requestPackagesList, + undefined, + { + config: { isGroupPage: false, resourceId: 1 }, + sorting, + filter: [{ type: 'type', value: { data: 'maven' } }], + }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(Api.projectPackages).toHaveBeenCalledWith(1, { + params: { + page: 1, + per_page: 20, + sort: sorting.sort, + order_by: sorting.orderBy, + package_type: packageType, + }, + }); + done(); + }, + ); + }); + + it('should create flash on API error', (done) => { + Api.projectPackages = jest.fn().mockRejectedValue(); + testAction( + actions.requestPackagesList, + undefined, + { config: { isGroupPage: false, resourceId: 2 }, sorting, filter }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + + it('should force the terraform_module type when forceTerraform is true', (done) => { + testAction( + actions.requestPackagesList, + undefined, + { config: { isGroupPage: false, resourceId: 1, forceTerraform: true }, sorting, filter }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(Api.projectPackages).toHaveBeenCalledWith(1, { + params: { + page: 1, + per_page: 20, + sort: sorting.sort, + order_by: sorting.orderBy, + package_type: 'terraform_module', + }, + }); + done(); + }, + ); + }); + }); + + describe('receivePackagesListSuccess', () => { + it('should set received packages', (done) => { + const data = 'foo'; + + testAction( + actions.receivePackagesListSuccess, + { data, headers }, + null, + [ + { type: types.SET_PACKAGE_LIST_SUCCESS, payload: data }, + { type: types.SET_PAGINATION, payload: headers }, + ], + [], + done, + ); + }); + }); + + describe('setInitialState', () => { + it('should commit setInitialState', (done) => { + testAction( + actions.setInitialState, + '1', + null, + [{ type: types.SET_INITIAL_STATE, payload: '1' }], + [], + done, + ); + }); + }); + + describe('setLoading', () => { + it('should commit set main loading', (done) => { + testAction( + actions.setLoading, + true, + null, + [{ type: types.SET_MAIN_LOADING, payload: true }], + [], + done, + ); + }); + }); + + describe('requestDeletePackage', () => { + const payload = { + _links: { + delete_api_path: 'foo', + }, + }; + it('should perform a delete operation on _links.delete_api_path', (done) => { + mock.onDelete(payload._links.delete_api_path).replyOnce(200); + Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' }); + + testAction( + actions.requestDeletePackage, + payload, + { pagination: { page: 1 } }, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'requestPackagesList', payload: { page: 1 } }, + ], + done, + ); + }); + + it('should stop the loading and call create flash on api error', (done) => { + mock.onDelete(payload._links.delete_api_path).replyOnce(400); + testAction( + actions.requestDeletePackage, + payload, + null, + [], + [ + { type: 'setLoading', payload: true }, + { type: 'setLoading', payload: false }, + ], + () => { + expect(createFlash).toHaveBeenCalled(); + done(); + }, + ); + }); + + it.each` + property | actionPayload + ${'_links'} | ${{}} + ${'delete_api_path'} | ${{ _links: {} }} + `('should reject and createFlash when $property is missing', ({ actionPayload }, done) => { + testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => { + expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR)); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); + done(); + }); + }); + }); + + describe('setSorting', () => { + it('should commit SET_SORTING', (done) => { + testAction( + actions.setSorting, + 'foo', + null, + [{ type: types.SET_SORTING, payload: 'foo' }], + [], + done, + ); + }); + }); + + describe('setFilter', () => { + it('should commit SET_FILTER', (done) => { + testAction( + actions.setFilter, + 'foo', + null, + [{ type: types.SET_FILTER, payload: 'foo' }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/getters_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/getters_spec.js new file mode 100644 index 00000000000..f2d52ace34e --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/getters_spec.js @@ -0,0 +1,36 @@ +import getList from '~/packages_and_registries/infrastructure_registry/list/stores/getters'; +import { packageList } from '../../mock_data'; + +describe('Getters registry list store', () => { + let state; + + const setState = ({ isGroupPage = false } = {}) => { + state = { + packages: packageList, + config: { + isGroupPage, + }, + }; + }; + + beforeEach(() => setState()); + + afterEach(() => { + state = null; + }); + + describe('getList', () => { + it('returns a list of packages', () => { + const result = getList(state); + + expect(result).toHaveLength(packageList.length); + expect(result[0].name).toBe('Test package'); + }); + + it('adds projectPathName', () => { + const result = getList(state); + + expect(result[0].projectPathName).toMatchInlineSnapshot(`"foo / bar / baz"`); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/mutations_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/mutations_spec.js new file mode 100644 index 00000000000..afd7a7e5439 --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/mutations_spec.js @@ -0,0 +1,87 @@ +import * as commonUtils from '~/lib/utils/common_utils'; +import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types'; +import mutations from '~/packages_and_registries/infrastructure_registry/list/stores/mutations'; +import createState from '~/packages_and_registries/infrastructure_registry/list/stores/state'; +import { npmPackage, mavenPackage } from '../../mock_data'; + +describe('Mutations Registry Store', () => { + let mockState; + beforeEach(() => { + mockState = createState(); + }); + + describe('SET_INITIAL_STATE', () => { + it('should set the initial state', () => { + const config = { + resourceId: '1', + pageType: 'groups', + userCanDelete: '', + emptyListIllustration: 'foo', + emptyListHelpUrl: 'baz', + }; + + const expectedState = { + ...mockState, + config: { + ...config, + isGroupPage: true, + canDestroyPackage: true, + }, + }; + mutations[types.SET_INITIAL_STATE](mockState, config); + + expect(mockState.projectId).toEqual(expectedState.projectId); + }); + }); + + describe('SET_PACKAGE_LIST_SUCCESS', () => { + it('should set a packages list', () => { + const payload = [npmPackage, mavenPackage]; + const expectedState = { ...mockState, packages: payload }; + mutations[types.SET_PACKAGE_LIST_SUCCESS](mockState, payload); + + expect(mockState.packages).toEqual(expectedState.packages); + }); + }); + + describe('SET_MAIN_LOADING', () => { + it('should set main loading', () => { + mutations[types.SET_MAIN_LOADING](mockState, true); + + expect(mockState.isLoading).toEqual(true); + }); + }); + + describe('SET_PAGINATION', () => { + const mockPagination = { perPage: 10, page: 1 }; + beforeEach(() => { + commonUtils.normalizeHeaders = jest.fn().mockReturnValue('baz'); + commonUtils.parseIntPagination = jest.fn().mockReturnValue(mockPagination); + }); + it('should set a parsed pagination', () => { + mutations[types.SET_PAGINATION](mockState, 'foo'); + expect(commonUtils.normalizeHeaders).toHaveBeenCalledWith('foo'); + expect(commonUtils.parseIntPagination).toHaveBeenCalledWith('baz'); + expect(mockState.pagination).toEqual(mockPagination); + }); + }); + + describe('SET_SORTING', () => { + it('should merge the sorting object with sort value', () => { + mutations[types.SET_SORTING](mockState, { sort: 'desc' }); + expect(mockState.sorting).toEqual({ ...mockState.sorting, sort: 'desc' }); + }); + + it('should merge the sorting object with order_by value', () => { + mutations[types.SET_SORTING](mockState, { orderBy: 'foo' }); + expect(mockState.sorting).toEqual({ ...mockState.sorting, orderBy: 'foo' }); + }); + }); + + describe('SET_FILTER', () => { + it('should set the filter query', () => { + mutations[types.SET_FILTER](mockState, 'foo'); + expect(mockState.filter).toEqual('foo'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/utils_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/utils_spec.js new file mode 100644 index 00000000000..a897fb90522 --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/utils_spec.js @@ -0,0 +1,51 @@ +import { SORT_FIELDS } from '~/packages_and_registries/infrastructure_registry/list/constants'; +import { + getNewPaginationPage, + sortableFields, +} from '~/packages_and_registries/infrastructure_registry/list/utils'; + +describe('Packages list utils', () => { + describe('sortableFields', () => { + it('returns the correct list when is a project page', () => { + expect(sortableFields()).toEqual(SORT_FIELDS.filter((f) => f.orderBy !== 'project_path')); + }); + it('returns the full list on the group page', () => { + expect(sortableFields(true)).toEqual(SORT_FIELDS); + }); + }); + describe('packageTypeDisplay', () => { + it('returns the current page when total items exceeds pagniation', () => { + expect(getNewPaginationPage(2, 20, 21)).toBe(2); + }); + + it('returns the previous page when total items is lower than or equal to pagination', () => { + expect(getNewPaginationPage(2, 20, 20)).toBe(1); + }); + + it('returns the first page when totalItems is lower than or equal to perPage', () => { + expect(getNewPaginationPage(4, 20, 20)).toBe(1); + }); + + describe('works when a different perPage is used', () => { + it('returns the current page', () => { + expect(getNewPaginationPage(2, 10, 11)).toBe(2); + }); + + it('returns the previous page', () => { + expect(getNewPaginationPage(2, 10, 10)).toBe(1); + }); + }); + + describe.each` + currentPage | totalItems | expectedResult + ${1} | ${20} | ${1} + ${2} | ${20} | ${1} + ${3} | ${40} | ${2} + ${4} | ${60} | ${3} + `(`works across numerious pages`, ({ currentPage, totalItems, expectedResult }) => { + it(`when currentPage is ${currentPage} return to the previous page ${expectedResult}`, () => { + expect(getNewPaginationPage(currentPage, 20, totalItems)).toBe(expectedResult); + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/mock_data.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/mock_data.js new file mode 100644 index 00000000000..33b47cca68b --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/mock_data.js @@ -0,0 +1,210 @@ +const _links = { + web_path: 'foo', + delete_api_path: 'bar', +}; + +export const mockPipelineInfo = { + id: 1, + ref: 'branch-name', + sha: 'sha-baz', + user: { + name: 'foo', + }, + project: { + name: 'foo-project', + web_url: 'foo-project-link', + commit_url: 'foo-commit-link', + pipeline_url: 'foo-pipeline-link', + }, + created_at: '2015-12-10', +}; + +export const mavenPackage = { + created_at: '2015-12-10', + id: 1, + maven_metadatum: { + app_group: 'com.test.app', + app_name: 'test-app', + app_version: '1.0-SNAPSHOT', + }, + name: 'Test package', + package_type: 'maven', + project_path: 'foo/bar/baz', + projectPathName: 'foo/bar/baz', + project_id: 1, + updated_at: '2015-12-10', + version: '1.0.0', + _links, +}; + +export const mavenFiles = [ + { + created_at: '2015-12-10', + file_name: 'File one', + id: 1, + size: 100, + download_path: '/-/package_files/1/download', + }, + { + created_at: '2015-12-10', + file_name: 'File two', + id: 2, + size: 200, + download_path: '/-/package_files/2/download', + }, +]; + +export const npmPackage = { + created_at: '2015-12-10', + id: 2, + name: '@Test/package', + package_type: 'npm', + project_path: 'foo/bar/baz', + projectPathName: 'foo/bar/baz', + project_id: 1, + updated_at: '2015-12-10', + version: '', + versions: [], + _links, + pipeline: mockPipelineInfo, +}; + +export const npmFiles = [ + { + created_at: '2015-12-10', + file_name: '@test/test-package-1.0.0.tgz', + id: 2, + size: 200, + download_path: '/-/package_files/2/download', + pipelines: [ + { id: 1, project: { commit_url: 'http://foo.bar' }, git_commit_message: 'foo bar baz?' }, + ], + file_sha256: 'file_sha256', + file_md5: 'file_md5', + file_sha1: 'file_sha1', + }, +]; + +export const conanPackage = { + conan_metadatum: { + package_channel: 'stable', + package_username: 'conan+conan-package', + }, + conan_package_name: 'conan-package', + created_at: '2015-12-10', + id: 3, + name: 'conan-package/1.0.0@conan+conan-package/stable', + project_path: 'foo/bar/baz', + projectPathName: 'foo/bar/baz', + package_files: [], + package_type: 'conan', + project_id: 1, + updated_at: '2015-12-10', + version: '1.0.0', + _links, +}; + +export const dependencyLinks = { + withoutFramework: { name: 'Moqi', version_pattern: '2.5.6' }, + withoutVersion: { name: 'Castle.Core', version_pattern: '' }, + fullLink: { + name: 'Test.Dependency', + version_pattern: '2.3.7', + target_framework: '.NETStandard2.0', + }, + anotherFullLink: { + name: 'Newtonsoft.Json', + version_pattern: '12.0.3', + target_framework: '.NETStandard2.0', + }, +}; + +export const nugetPackage = { + created_at: '2015-12-10', + id: 4, + name: 'NugetPackage1', + package_files: [], + package_type: 'nuget', + project_id: 1, + tags: [], + updated_at: '2015-12-10', + version: '1.0.0', + dependency_links: Object.values(dependencyLinks), + nuget_metadatum: { + icon_url: 'fake-icon', + project_url: 'project-foo-url', + license_url: 'license-foo-url', + }, +}; + +export const rubygemsPackage = { + created_at: '2015-12-10', + id: 4, + name: 'RubyGem1', + package_files: [], + package_type: 'rubygems', + project_id: 1, + tags: [], + updated_at: '2015-12-10', + version: '1.0.0', + rubygems_metadatum: { + author: 'Fake Name', + summary: 'My gem', + email: 'tanuki@fake.com', + }, +}; + +export const pypiPackage = { + created_at: '2015-12-10', + id: 5, + name: 'PyPiPackage', + package_files: [], + package_type: 'pypi', + project_id: 1, + tags: [], + updated_at: '2015-12-10', + version: '1.0.0', +}; + +export const composerPackage = { + created_at: '2015-12-10', + id: 5, + name: 'ComposerPackage', + package_files: [], + package_type: 'composer', + project_id: 1, + tags: [], + updated_at: '2015-12-10', + version: '1.0.0', +}; + +export const terraformModule = { + created_at: '2015-12-10', + id: 2, + name: 'Test/system-22', + package_type: 'terraform_module', + project_path: 'foo/bar/baz', + projectPathName: 'foo/bar/baz', + project_id: 1, + updated_at: '2015-12-10', + version: '0.1', + versions: [], + _links, +}; + +export const mockTags = [ + { + name: 'foo-1', + }, + { + name: 'foo-2', + }, + { + name: 'foo-3', + }, + { + name: 'foo-4', + }, +]; + +export const packageList = [mavenPackage, { ...npmPackage, tags: mockTags }, conanPackage]; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap new file mode 100644 index 00000000000..67c3b8b795a --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`packages_list_row renders 1`] = ` +<div + class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100" + data-qa-selector="package_row" +> + <div + class="gl-display-flex gl-align-items-center gl-py-3" + > + <!----> + + <div + class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" + > + <div + class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1" + > + <div + class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0" + > + <div + class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0" + > + <gl-link-stub + class="gl-text-body gl-min-w-0" + data-qa-selector="package_link" + href="foo" + > + <gl-truncate-stub + position="end" + text="Test package" + /> + </gl-link-stub> + + <!----> + + <!----> + </div> + + <!----> + </div> + + <div + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1" + > + <div + class="gl-display-flex" + > + <span> + 1.0.0 + </span> + + <!----> + + <div /> + + <package-path-stub + path="foo/bar/baz" + /> + </div> + </div> + </div> + + <div + class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0" + > + <div + class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6" + > + <publish-method-stub + packageentity="[object Object]" + /> + </div> + + <div + class="gl-display-flex gl-align-items-center gl-min-h-6" + > + <span> + <gl-sprintf-stub + message="Created %{timestamp}" + /> + </span> + </div> + </div> + </div> + + <div + class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1" + > + <gl-button-stub + aria-label="Remove package" + buttontextclasses="" + category="secondary" + data-testid="action-delete" + icon="remove" + size="medium" + title="Remove package" + variant="danger" + /> + </div> + </div> + + <div + class="gl-display-flex" + > + <div + class="gl-w-7" + /> + + <!----> + + <div + class="gl-w-9" + /> + </div> +</div> +`; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js index ef26c729691..abb0d23b6e4 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/infrastructure_icon_and_name_spec.js @@ -1,6 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue'; +import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue'; describe('InfrastructureIconAndName', () => { let wrapper; diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js new file mode 100644 index 00000000000..1052fdd1dda --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js @@ -0,0 +1,161 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/shared/constants'; + +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import { packageList } from '../mock_data'; + +describe('packages_list_row', () => { + let wrapper; + let store; + + const [packageWithoutTags, packageWithTags] = packageList; + + const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' }; + + const findPackageTags = () => wrapper.findComponent(PackageTags); + const findPackagePath = () => wrapper.findComponent(PackagePath); + const findDeleteButton = () => wrapper.findByTestId('action-delete'); + const findInfrastructureIconAndName = () => wrapper.findComponent(InfrastructureIconAndName); + const findListItem = () => wrapper.findComponent(ListItem); + const findPackageLink = () => wrapper.findComponent(GlLink); + const findWarningIcon = () => wrapper.findByTestId('warning-icon'); + + const mountComponent = ({ + isGroup = false, + packageEntity = packageWithoutTags, + showPackageType = true, + disableDelete = false, + provide, + } = {}) => { + wrapper = shallowMountExtended(PackagesListRow, { + store, + provide, + stubs: { + ListItem, + InfrastructureIconAndName, + }, + propsData: { + packageLink: 'foo', + packageEntity, + isGroup, + showPackageType, + disableDelete, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders', () => { + mountComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('tags', () => { + it('renders package tags when a package has tags', () => { + mountComponent({ isGroup: false, packageEntity: packageWithTags }); + + expect(findPackageTags().exists()).toBe(true); + }); + + it('does not render when there are no tags', () => { + mountComponent(); + + expect(findPackageTags().exists()).toBe(false); + }); + }); + + describe('when is is group', () => { + it('has a package path component', () => { + mountComponent({ isGroup: true }); + + expect(findPackagePath().exists()).toBe(true); + expect(findPackagePath().props()).toMatchObject({ path: 'foo/bar/baz' }); + }); + }); + + describe('showPackageType', () => { + it('shows the type when set', () => { + mountComponent(); + + expect(findInfrastructureIconAndName().exists()).toBe(true); + }); + + it('does not show the type when not set', () => { + mountComponent({ showPackageType: false }); + + expect(findInfrastructureIconAndName().exists()).toBe(false); + }); + }); + + describe('deleteAvailable', () => { + it('does not show when not set', () => { + mountComponent({ disableDelete: true }); + + expect(findDeleteButton().exists()).toBe(false); + }); + }); + + describe('delete button', () => { + it('exists and has the correct props', () => { + mountComponent({ packageEntity: packageWithoutTags }); + + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().attributes()).toMatchObject({ + icon: 'remove', + category: 'secondary', + variant: 'danger', + title: 'Remove package', + }); + }); + + it('emits the packageToDelete event when the delete button is clicked', async () => { + mountComponent({ packageEntity: packageWithoutTags }); + + findDeleteButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + expect(wrapper.emitted('packageToDelete')).toBeTruthy(); + expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]); + }); + }); + + describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { + beforeEach(() => { + mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } }); + }); + + it('list item has a disabled prop', () => { + expect(findListItem().props('disabled')).toBe(true); + }); + + it('details link is disabled', () => { + expect(findPackageLink().attributes('disabled')).toBe('true'); + }); + + it('has a warning icon', () => { + const icon = findWarningIcon(); + const tooltip = getBinding(icon.element, 'gl-tooltip'); + expect(icon.props('icon')).toBe('warning'); + expect(tooltip.value).toMatchObject({ + title: 'Invalid Package: failed metadata extraction', + }); + }); + + it('delete button is disabled', () => { + expect(findDeleteButton().props('disabled')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap index c95538546c1..7aa42a1f1e5 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap @@ -5,7 +5,7 @@ exports[`VersionRow renders 1`] = ` class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100" > <div - class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5" + class="gl-display-flex gl-align-items-center gl-py-3" > <!----> diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js index d59c3184e4e..6ad6007c9da 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js @@ -2,7 +2,7 @@ import { GlIcon, GlSprintf } from '@gitlab/ui'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import { PACKAGE_TYPE_CONAN, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js index f7613949fe4..faeca76d746 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js @@ -1,8 +1,8 @@ import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; -import PublishMethod from '~/packages/shared/components/publish_method.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap index 2f2be797251..165ee962417 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap @@ -6,7 +6,7 @@ exports[`packages_list_row renders 1`] = ` data-qa-selector="package_row" > <div - class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5" + class="gl-display-flex gl-align-items-center gl-py-3" > <!----> @@ -77,7 +77,9 @@ exports[`packages_list_row renders 1`] = ` <div class="gl-display-flex gl-align-items-center gl-min-h-6" > - <span> + <span + data-testid="created-date" + > Created <timeago-tooltip-stub cssclass="" @@ -90,7 +92,7 @@ exports[`packages_list_row renders 1`] = ` </div> <div - class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1" + class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1" > <gl-button-stub aria-label="Remove package" diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap index 919dbe25ffe..4407c4a2003 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap @@ -37,6 +37,7 @@ exports[`publish_method renders 1`] = ` text="b83d6e391c22777fca1ed3012fce84f633d7fed0" title="Copy commit SHA" tooltipplacement="top" + variant="default" /> </div> `; diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index a276db104d7..292667ec47c 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -3,9 +3,11 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; -import PackagePath from '~/packages/shared/components/package_path.vue'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; -import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue'; +import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; @@ -29,6 +31,9 @@ describe('packages_list_row', () => { const findPackageLink = () => wrapper.findComponent(GlLink); const findWarningIcon = () => wrapper.findByTestId('warning-icon'); const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos'); + const findPublishMethod = () => wrapper.findComponent(PublishMethod); + const findCreatedDateText = () => wrapper.findByTestId('created-date'); + const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); const mountComponent = ({ packageEntity = packageWithoutTags, @@ -153,4 +158,23 @@ describe('packages_list_row', () => { expect(findPackageIconAndName().text()).toBe(packageWithoutTags.packageType.toLowerCase()); }); }); + + describe('right info', () => { + it('has publish method component', () => { + mountComponent({ + packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } }, + }); + + expect(findPublishMethod().props('pipeline')).toEqual(packagePipelines()[0]); + }); + + it('has the created date', () => { + mountComponent(); + + expect(findCreatedDateText().text()).toMatchInterpolatedText(PackagesListRow.i18n.createdAt); + expect(findTimeAgoTooltip().props()).toMatchObject({ + time: packageData().createdAt, + }); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js index de4e9c8ae5b..97978dee909 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js @@ -1,8 +1,8 @@ import { GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import PackagesListRow from '~/packages/shared/components/package_list_row.vue'; -import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; +import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import { DELETE_PACKAGE_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_TRACKING_ACTION, diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index bacc748db81..4c23b52b8a2 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -16,11 +16,13 @@ export const packagePipelines = (extend) => [ ref: 'master', sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', project: { + id: '1', name: 'project14', webUrl: 'http://gdk.test:3000/namespace14/project14', __typename: 'Project', }, user: { + id: 'user-1', name: 'Administrator', }, ...extend, @@ -89,6 +91,7 @@ export const dependencyLinks = () => [ ]; export const packageProject = () => ({ + id: '1', fullPath: 'gitlab-org/gitlab-test', webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-test', __typename: 'Project', @@ -127,6 +130,7 @@ export const packageData = (extend) => ({ }); export const conanMetadata = () => ({ + id: 'conan-1', packageChannel: 'stable', packageUsername: 'gitlab-org+gitlab-test', recipe: 'package-8/1.0.0@gitlab-org+gitlab-test/stable', @@ -179,6 +183,7 @@ export const packageDetailsQuery = (extendPackage) => ({ ...nugetMetadata(), }, project: { + id: '1', path: 'projectPath', }, tags: { @@ -270,6 +275,7 @@ export const packageDestroyFileMutationError = () => ({ export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({ data: { [type]: { + id: '1', packages: { count: 2, nodes: [ diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap index 5af75868084..dbe3c70c3cb 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap @@ -4,7 +4,7 @@ exports[`PackagesListApp renders 1`] = ` <div> <package-title-stub count="2" - helpurl="packageHelpUrl" + helpurl="/help/user/packages/index" /> <package-search-stub /> @@ -35,17 +35,21 @@ exports[`PackagesListApp renders 1`] = ` class="text-content gl-mx-auto gl-my-0 gl-p-5" > <h1 - class="h4" + class="gl-font-size-h-display gl-line-height-36 h4" > - There are no packages yet + + There are no packages yet + </h1> - <p> + <p + class="gl-mt-3" + > Learn how to <b-link-stub class="gl-link" event="click" - href="emptyListHelpUrl" + href="/help/user/packages/package_registry/index" routertag="a" target="_blank" > diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js index ad848f367e0..2ac2a6455ef 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js @@ -6,7 +6,7 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue'; +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'; @@ -16,11 +16,13 @@ import { PROJECT_RESOURCE_TYPE, GROUP_RESOURCE_TYPE, GRAPHQL_PAGE_SIZE, + EMPTY_LIST_HELP_URL, + PACKAGE_HELP_URL, } from '~/packages_and_registries/package_registry/constants'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; -import { packagesListQuery, packageData, pagination } from '../../mock_data'; +import { packagesListQuery, packageData, pagination } from '../mock_data'; jest.mock('~/lib/utils/common_utils'); jest.mock('~/flash'); @@ -32,9 +34,7 @@ describe('PackagesListApp', () => { let apolloProvider; const defaultProvide = { - packageHelpUrl: 'packageHelpUrl', emptyListIllustration: 'emptyListIllustration', - emptyListHelpUrl: 'emptyListHelpUrl', isGroupPage: true, fullPath: 'gitlab-org', }; @@ -66,7 +66,7 @@ describe('PackagesListApp', () => { const requestHandlers = [[getPackagesQuery, resolver]]; apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(PackageListApp, { + wrapper = shallowMountExtended(ListPage, { localVue, apolloProvider, provide, @@ -113,7 +113,10 @@ describe('PackagesListApp', () => { await waitForFirstRequest(); expect(findPackageTitle().exists()).toBe(true); - expect(findPackageTitle().props('count')).toBe(2); + expect(findPackageTitle().props()).toMatchObject({ + count: 2, + helpUrl: PACKAGE_HELP_URL, + }); }); describe('search component', () => { @@ -213,12 +216,12 @@ describe('PackagesListApp', () => { it('generate the correct empty list link', () => { const link = findListComponent().findComponent(GlLink); - expect(link.attributes('href')).toBe(defaultProvide.emptyListHelpUrl); + 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(PackageListApp.i18n.emptyPageTitle); + expect(findEmptyState().text()).toContain(ListPage.i18n.emptyPageTitle); }); }); @@ -234,8 +237,8 @@ describe('PackagesListApp', () => { }); it('should show specific empty message', () => { - expect(findEmptyState().text()).toContain(PackageListApp.i18n.noResultsTitle); - expect(findEmptyState().text()).toContain(PackageListApp.i18n.widenFilters); + expect(findEmptyState().text()).toContain(ListPage.i18n.noResultsTitle); + expect(findEmptyState().text()).toContain(ListPage.i18n.widenFilters); }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap index f2087733d2b..5b56cb7f74e 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap +++ b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap @@ -3,7 +3,7 @@ exports[`settings_titles renders properly 1`] = ` <div> <h5 - class="gl-border-b-solid gl-border-b-1 gl-border-gray-200" + class="gl-border-b-solid gl-border-b-1 gl-border-gray-200 gl-pb-3" > foo diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js index d3a970e86eb..f6c1d212b51 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js @@ -1,6 +1,7 @@ -import { GlSprintf, GlLink, GlToggle } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlSprintf, GlToggle } from '@gitlab/ui'; +import { createLocalVue } from '@vue/test-utils'; 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'; @@ -12,14 +13,21 @@ import { } from '~/packages_and_registries/settings/group/constants'; import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql'; +import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql'; import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; -import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; +import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; import { - dependencyProxySettings, + updateGroupDependencyProxySettingsOptimisticResponse, + updateDependencyProxyImageTtlGroupPolicyOptimisticResponse, +} from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; +import { + dependencyProxySettings as dependencyProxySettingsMock, + dependencyProxyImageTtlPolicy as dependencyProxyImageTtlPolicyMock, dependencyProxySettingMutationMock, groupPackageSettingsMock, - dependencyProxySettingMutationErrorMock, + mutationErrorMock, + dependencyProxyUpdateTllPolicyMutationMock, } from '../mock_data'; jest.mock('~/flash'); @@ -30,46 +38,68 @@ const localVue = createLocalVue(); describe('DependencyProxySettings', () => { let wrapper; let apolloProvider; + let updateSettingsMutationResolver; + let updateTtlPoliciesMutationResolver; const defaultProvide = { defaultExpanded: false, groupPath: 'foo_group_path', + groupDependencyProxyPath: 'group_dependency_proxy_path', }; localVue.use(VueApollo); const mountComponent = ({ provide = defaultProvide, - mutationResolver = jest.fn().mockResolvedValue(dependencyProxySettingMutationMock()), isLoading = false, + dependencyProxySettings = dependencyProxySettingsMock(), + dependencyProxyImageTtlPolicy = dependencyProxyImageTtlPolicyMock(), } = {}) => { - const requestHandlers = [[updateDependencyProxySettings, mutationResolver]]; + const requestHandlers = [ + [updateDependencyProxySettings, updateSettingsMutationResolver], + [updateDependencyProxyImageTtlGroupPolicy, updateTtlPoliciesMutationResolver], + ]; apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMount(component, { + wrapper = shallowMountExtended(component, { localVue, apolloProvider, provide, propsData: { - dependencyProxySettings: dependencyProxySettings(), + dependencyProxySettings, + dependencyProxyImageTtlPolicy, isLoading, }, stubs: { GlSprintf, + GlToggle, SettingsBlock, }, }); }; + beforeEach(() => { + updateSettingsMutationResolver = jest + .fn() + .mockResolvedValue(dependencyProxySettingMutationMock()); + updateTtlPoliciesMutationResolver = jest + .fn() + .mockResolvedValue(dependencyProxyUpdateTllPolicyMutationMock()); + }); + afterEach(() => { wrapper.destroy(); }); const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); - const findDescription = () => wrapper.find('[data-testid="description"'); - const findLink = () => wrapper.findComponent(GlLink); - const findToggle = () => wrapper.findComponent(GlToggle); + const findSettingsTitles = () => wrapper.findComponent(SettingsTitles); + const findDescription = () => wrapper.findByTestId('description'); + const findDescriptionLink = () => wrapper.findByTestId('description-link'); + const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle'); + const findEnableTtlPoliciesToggle = () => + wrapper.findByTestId('dependency-proxy-ttl-policies-toggle'); + const findToggleHelpLink = () => wrapper.findByTestId('toggle-help-link'); const fillApolloCache = () => { apolloProvider.defaultClient.cache.writeQuery({ @@ -81,10 +111,6 @@ describe('DependencyProxySettings', () => { }); }; - const emitSettingsUpdate = (value = false) => { - findToggle().vm.$emit('change', value); - }; - it('renders a settings block', () => { mountComponent(); @@ -112,19 +138,93 @@ describe('DependencyProxySettings', () => { it('has the correct link', () => { mountComponent(); - expect(findLink().attributes()).toMatchObject({ + expect(findDescriptionLink().attributes()).toMatchObject({ href: DEPENDENCY_PROXY_DOCS_PATH, }); - expect(findLink().text()).toBe('Learn more'); + expect(findDescriptionLink().text()).toBe('Learn more'); + }); + + describe('enable toggle', () => { + it('exists', () => { + mountComponent(); + + expect(findEnableProxyToggle().props()).toMatchObject({ + label: component.i18n.enabledProxyLabel, + }); + }); + + describe('when enabled', () => { + beforeEach(() => { + mountComponent(); + }); + + it('has the help prop correctly set', () => { + expect(findEnableProxyToggle().props()).toMatchObject({ + help: component.i18n.enabledProxyHelpText, + }); + }); + + it('has help text with a link', () => { + expect(findEnableProxyToggle().text()).toContain( + 'To see the image prefix and what is in the cache, visit the Dependency Proxy', + ); + expect(findToggleHelpLink().attributes()).toMatchObject({ + href: defaultProvide.groupDependencyProxyPath, + }); + }); + }); + + describe('when disabled', () => { + beforeEach(() => { + mountComponent({ + dependencyProxySettings: dependencyProxySettingsMock({ enabled: false }), + }); + }); + + it('has the help prop set to empty', () => { + expect(findEnableProxyToggle().props()).toMatchObject({ + help: '', + }); + }); + + it('the help text is not visible', () => { + expect(findToggleHelpLink().exists()).toBe(false); + }); + }); + }); + + describe('storage settings', () => { + it('the component has the settings title', () => { + mountComponent(); + + expect(findSettingsTitles().props()).toMatchObject({ + title: component.i18n.storageSettingsTitle, + }); + }); + + describe('enable proxy ttl policies', () => { + it('exists', () => { + mountComponent(); + + expect(findEnableTtlPoliciesToggle().props()).toMatchObject({ + label: component.i18n.ttlPolicyEnabledLabel, + help: component.i18n.ttlPolicyEnabledHelpText, + }); + }); + }); }); - describe('settings update', () => { + describe.each` + toggleName | toggleFinder | localErrorMock | optimisticResponse + ${'enable proxy'} | ${findEnableProxyToggle} | ${dependencyProxySettingMutationMock} | ${updateGroupDependencyProxySettingsOptimisticResponse} + ${'enable ttl policies'} | ${findEnableTtlPoliciesToggle} | ${dependencyProxyUpdateTllPolicyMutationMock} | ${updateDependencyProxyImageTtlGroupPolicyOptimisticResponse} + `('$toggleName settings update ', ({ optimisticResponse, toggleFinder, localErrorMock }) => { describe('success state', () => { it('emits a success event', async () => { mountComponent(); fillApolloCache(); - emitSettingsUpdate(); + toggleFinder().vm.$emit('change', false); await waitForPromises(); @@ -136,26 +236,28 @@ describe('DependencyProxySettings', () => { fillApolloCache(); - expect(findToggle().props('value')).toBe(true); + expect(toggleFinder().props('value')).toBe(true); - emitSettingsUpdate(); + toggleFinder().vm.$emit('change', false); - expect(updateGroupDependencyProxySettingsOptimisticResponse).toHaveBeenCalledWith({ - enabled: false, - }); + expect(optimisticResponse).toHaveBeenCalledWith( + expect.objectContaining({ + enabled: false, + }), + ); }); }); describe('errors', () => { it('mutation payload with root level errors', async () => { - const mutationResolver = jest - .fn() - .mockResolvedValue(dependencyProxySettingMutationErrorMock); - mountComponent({ mutationResolver }); + updateSettingsMutationResolver = jest.fn().mockResolvedValue(mutationErrorMock); + updateTtlPoliciesMutationResolver = jest.fn().mockResolvedValue(mutationErrorMock); + + mountComponent(); fillApolloCache(); - emitSettingsUpdate(); + toggleFinder().vm.$emit('change', false); await waitForPromises(); @@ -163,14 +265,16 @@ describe('DependencyProxySettings', () => { }); it.each` - type | mutationResolver - ${'local'} | ${jest.fn().mockResolvedValue(dependencyProxySettingMutationMock({ errors: ['foo'] }))} + type | mutationResolverMock + ${'local'} | ${jest.fn().mockResolvedValue(localErrorMock({ errors: ['foo'] }))} ${'network'} | ${jest.fn().mockRejectedValue()} - `('mutation payload with $type error', async ({ mutationResolver }) => { - mountComponent({ mutationResolver }); + `('mutation payload with $type error', async ({ mutationResolverMock }) => { + updateSettingsMutationResolver = mutationResolverMock; + updateTtlPoliciesMutationResolver = mutationResolverMock; + mountComponent(); fillApolloCache(); - emitSettingsUpdate(); + toggleFinder().vm.$emit('change', false); await waitForPromises(); @@ -180,10 +284,16 @@ describe('DependencyProxySettings', () => { }); describe('when isLoading is true', () => { - it('disables enable toggle', () => { + it('disables enable proxy toggle', () => { + mountComponent({ isLoading: true }); + + expect(findEnableProxyToggle().props('disabled')).toBe(true); + }); + + it('disables enable ttl policies toggle', () => { mountComponent({ isLoading: true }); - expect(findToggle().props('disabled')).toBe(true); + expect(findEnableTtlPoliciesToggle().props('disabled')).toBe(true); }); }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js index e4d62bc6a6e..933dac7f5a8 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js @@ -10,7 +10,12 @@ import DependencyProxySettings from '~/packages_and_registries/settings/group/co import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue'; import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; -import { groupPackageSettingsMock, packageSettings, dependencyProxySettings } from '../mock_data'; +import { + groupPackageSettingsMock, + packageSettings, + dependencyProxySettings, + dependencyProxyImageTtlPolicy, +} from '../mock_data'; jest.mock('~/flash'); @@ -66,11 +71,17 @@ describe('Group Settings App', () => { await nextTick(); }; + const packageSettingsProps = { packageSettings: packageSettings() }; + const dependencyProxyProps = { + dependencyProxySettings: dependencyProxySettings(), + dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(), + }; + describe.each` - finder | entityProp | entityValue | successMessage | errorMessage - ${findPackageSettings} | ${'packageSettings'} | ${packageSettings()} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'} - ${findDependencyProxySettings} | ${'dependencyProxySettings'} | ${dependencyProxySettings()} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'} - `('settings blocks', ({ finder, entityProp, entityValue, successMessage, errorMessage }) => { + finder | entitySpecificProps | successMessage | errorMessage + ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'} + ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'} + `('settings blocks', ({ finder, entitySpecificProps, successMessage, errorMessage }) => { beforeEach(() => { mountComponent(); return waitForApolloQueryAndRender(); @@ -83,7 +94,7 @@ describe('Group Settings App', () => { it('binds the correctProps', () => { expect(finder().props()).toMatchObject({ isLoading: false, - [entityProp]: entityValue, + ...entitySpecificProps, }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js index a61edad8685..fcfad4b42b8 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js @@ -4,15 +4,19 @@ import SettingsTitles from '~/packages_and_registries/settings/group/components/ describe('settings_titles', () => { let wrapper; - const mountComponent = () => { + const defaultProps = { + title: 'foo', + subTitle: 'bar', + }; + + const mountComponent = (propsData = defaultProps) => { wrapper = shallowMount(SettingsTitles, { - propsData: { - title: 'foo', - subTitle: 'bar', - }, + propsData, }); }; + const findSubTitle = () => wrapper.find('p'); + afterEach(() => { wrapper.destroy(); }); @@ -22,4 +26,10 @@ describe('settings_titles', () => { expect(wrapper.element).toMatchSnapshot(); }); + + it('does not render the subtitle paragraph when no subtitle is passed', () => { + mountComponent({ title: defaultProps.title }); + + expect(findSubTitle().exists()).toBe(false); + }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js index 9d8504a1124..a5b571a0241 100644 --- a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js @@ -17,6 +17,13 @@ describe('Package and Registries settings group cache updates', () => { }, }; + const updateDependencyProxyImageTtlGroupPolicyPayload = { + dependencyProxyImageTtlPolicy: { + enabled: false, + ttl: 45, + }, + }; + const cacheMock = { group: { packageSettings: { @@ -26,6 +33,10 @@ describe('Package and Registries settings group cache updates', () => { dependencyProxySetting: { enabled: true, }, + dependencyProxyImageTtlPolicy: { + enabled: true, + ttl: 45, + }, }, }; @@ -42,15 +53,26 @@ describe('Package and Registries settings group cache updates', () => { }); describe.each` - updateNamespacePackageSettings | updateDependencyProxySettings - ${updateNamespacePackageSettingsPayload} | ${updateDependencyProxySettingsPayload} - ${undefined} | ${updateDependencyProxySettingsPayload} - ${updateNamespacePackageSettingsPayload} | ${undefined} - ${undefined} | ${undefined} + updateNamespacePackageSettings | updateDependencyProxySettings | updateDependencyProxyImageTtlGroupPolicy + ${updateNamespacePackageSettingsPayload} | ${updateDependencyProxySettingsPayload} | ${undefined} + ${undefined} | ${updateDependencyProxySettingsPayload} | ${undefined} + ${updateNamespacePackageSettingsPayload} | ${undefined} | ${undefined} + ${undefined} | ${undefined} | ${updateDependencyProxyImageTtlGroupPolicyPayload} + ${undefined} | ${undefined} | ${undefined} `( 'updateGroupPackageSettings', - ({ updateNamespacePackageSettings, updateDependencyProxySettings }) => { - const payload = { data: { updateNamespacePackageSettings, updateDependencyProxySettings } }; + ({ + updateNamespacePackageSettings, + updateDependencyProxySettings, + updateDependencyProxyImageTtlGroupPolicy, + }) => { + const payload = { + data: { + updateNamespacePackageSettings, + updateDependencyProxySettings, + updateDependencyProxyImageTtlGroupPolicy, + }, + }; it('calls readQuery', () => { updateGroupPackageSettings('foo')(client, payload); expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables); @@ -65,6 +87,7 @@ describe('Package and Registries settings group cache updates', () => { ...cacheMock.group, ...payload.data.updateNamespacePackageSettings, ...payload.data.updateDependencyProxySettings, + ...payload.data.updateDependencyProxyImageTtlGroupPolicy, }, }, }); diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js index debeb9aa89c..b4efda3e7b2 100644 --- a/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js @@ -1,6 +1,7 @@ import { updateGroupPackagesSettingsOptimisticResponse, updateGroupDependencyProxySettingsOptimisticResponse, + updateDependencyProxyImageTtlGroupPolicyOptimisticResponse, } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; describe('Optimistic responses', () => { @@ -38,4 +39,22 @@ describe('Optimistic responses', () => { `); }); }); + + describe('updateDependencyProxyImageTtlGroupPolicyOptimisticResponse', () => { + it('returns the correct structure', () => { + expect(updateDependencyProxyImageTtlGroupPolicyOptimisticResponse({ foo: 'bar' })) + .toMatchInlineSnapshot(` + Object { + "__typename": "Mutation", + "updateDependencyProxyImageTtlGroupPolicy": Object { + "__typename": "UpdateDependencyProxyImageTtlGroupPolicyPayload", + "dependencyProxyImageTtlPolicy": Object { + "foo": "bar", + }, + "errors": Array [], + }, + } + `); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js index 81ba0795b7d..d53446de910 100644 --- a/spec/frontend/packages_and_registries/settings/group/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js @@ -5,16 +5,25 @@ export const packageSettings = () => ({ genericDuplicateExceptionRegex: '', }); -export const dependencyProxySettings = () => ({ +export const dependencyProxySettings = (extend) => ({ enabled: true, + ...extend, +}); + +export const dependencyProxyImageTtlPolicy = (extend) => ({ + ttl: 90, + enabled: true, + ...extend, }); export const groupPackageSettingsMock = { data: { group: { + id: '1', fullPath: 'foo_group_path', packageSettings: packageSettings(), dependencyProxySetting: dependencyProxySettings(), + dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(), }, }, }; @@ -44,6 +53,16 @@ export const dependencyProxySettingMutationMock = (override) => ({ }, }); +export const dependencyProxyUpdateTllPolicyMutationMock = (override) => ({ + data: { + updateDependencyProxyImageTtlGroupPolicy: { + dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(), + errors: [], + ...override, + }, + }, +}); + export const groupPackageSettingsMutationErrorMock = { errors: [ { @@ -68,7 +87,8 @@ export const groupPackageSettingsMutationErrorMock = { }, ], }; -export const dependencyProxySettingMutationErrorMock = { + +export const mutationErrorMock = { errors: [ { message: 'Some error', diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js index 9778f409010..a56bb75f8ed 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js @@ -11,6 +11,7 @@ export const containerExpirationPolicyData = () => ({ export const expirationPolicyPayload = (override) => ({ data: { project: { + id: '1', containerExpirationPolicy: { ...containerExpirationPolicyData(), ...override, diff --git a/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap new file mode 100644 index 00000000000..5f243799bae --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`publish_method renders 1`] = ` +<div + class="gl-display-flex gl-align-items-center" +> + <gl-icon-stub + class="gl-mr-2" + name="git-merge" + size="16" + /> + + <span + class="gl-mr-2" + data-testid="pipeline-ref" + > + branch-name + </span> + + <gl-icon-stub + class="gl-mr-2" + name="commit" + size="16" + /> + + <gl-link-stub + class="gl-mr-2" + data-testid="pipeline-sha" + href="../commit/sha-baz" + > + sha-baz + </gl-link-stub> + + <clipboard-button-stub + category="tertiary" + size="small" + text="sha-baz" + title="Copy commit SHA" + tooltipplacement="top" + variant="default" + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js new file mode 100644 index 00000000000..aaca58d21bb --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js @@ -0,0 +1,199 @@ +import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import component from '~/packages_and_registries/shared/components/registry_list.vue'; + +describe('Registry List', () => { + let wrapper; + + const items = [{ id: 'a' }, { id: 'b' }]; + const defaultPropsData = { + title: 'test_title', + items, + }; + + const rowScopedSlot = ` + <div data-testid="scoped-slot"> + <button @click="props.selectItem(props.item)">Select</button> + <span>{{props.first}}</span> + <p>{{props.isSelected(props.item)}}</p> + </div>`; + + const mountComponent = ({ propsData = defaultPropsData } = {}) => { + wrapper = shallowMountExtended(component, { + propsData, + scopedSlots: { + default: rowScopedSlot, + }, + }); + }; + + const findSelectAll = () => wrapper.findComponent(GlFormCheckbox); + const findDeleteSelected = () => wrapper.findComponent(GlButton); + const findPagination = () => wrapper.findComponent(GlKeysetPagination); + const findScopedSlots = () => wrapper.findAllByTestId('scoped-slot'); + const findScopedSlotSelectButton = (index) => findScopedSlots().at(index).find('button'); + const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span'); + const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('header', () => { + it('renders the title passed in the prop', () => { + mountComponent(); + + expect(wrapper.text()).toContain(defaultPropsData.title); + }); + + describe('select all checkbox', () => { + beforeEach(() => { + mountComponent(); + }); + + it('exists', () => { + expect(findSelectAll().exists()).toBe(true); + }); + + it('select and unselect all', async () => { + // no row is not selected + items.forEach((item, index) => { + expect(findScopedSlotIsSelectedValue(index).text()).toBe(''); + }); + + // simulate selection + findSelectAll().vm.$emit('input', true); + await nextTick(); + + // all rows selected + items.forEach((item, index) => { + expect(findScopedSlotIsSelectedValue(index).text()).toBe('true'); + }); + + // simulate de-selection + findSelectAll().vm.$emit('input', ''); + await nextTick(); + + // no row is not selected + items.forEach((item, index) => { + expect(findScopedSlotIsSelectedValue(index).text()).toBe(''); + }); + }); + }); + + describe('delete button', () => { + it('has the correct text', () => { + mountComponent(); + + expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected); + }); + + it('is hidden when hiddenDelete is true', () => { + mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } }); + + expect(findDeleteSelected().exists()).toBe(false); + }); + + it('is disabled when isLoading is true', () => { + mountComponent({ propsData: { ...defaultPropsData, isLoading: true } }); + + expect(findDeleteSelected().props('disabled')).toBe(true); + }); + + it('is disabled when no row is selected', async () => { + mountComponent(); + + expect(findDeleteSelected().props('disabled')).toBe(true); + + await findScopedSlotSelectButton(0).trigger('click'); + + expect(findDeleteSelected().props('disabled')).toBe(false); + }); + + it('on click emits the delete event with the selected rows', async () => { + mountComponent(); + + await findScopedSlotSelectButton(0).trigger('click'); + + findDeleteSelected().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[[items[0]]]]); + }); + }); + }); + + describe('main area', () => { + beforeEach(() => { + mountComponent(); + }); + + it('renders scopedSlots based on the items props', () => { + expect(findScopedSlots()).toHaveLength(items.length); + }); + + it('populates the scope of the slot correctly', async () => { + expect(findScopedSlots().at(0).exists()).toBe(true); + + // it's the first slot + expect(findScopedSlotFirstValue(0).text()).toBe('true'); + + // item is not selected, falsy is translated to empty string + expect(findScopedSlotIsSelectedValue(0).text()).toBe(''); + + // find the button with the bound function + await findScopedSlotSelectButton(0).trigger('click'); + + // the item is selected + expect(findScopedSlotIsSelectedValue(0).text()).toBe('true'); + }); + }); + + describe('footer', () => { + let pagination; + + beforeEach(() => { + pagination = { hasPreviousPage: false, hasNextPage: true }; + }); + + it('has a pagination', () => { + mountComponent({ + propsData: { ...defaultPropsData, pagination }, + }); + + expect(findPagination().props()).toMatchObject(pagination); + }); + + it.each` + hasPreviousPage | hasNextPage | visible + ${true} | ${true} | ${true} + ${true} | ${false} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${false} + `( + 'when hasPreviousPage is $hasPreviousPage and hasNextPage is $hasNextPage is $visible that the pagination is shown', + ({ hasPreviousPage, hasNextPage, visible }) => { + pagination = { hasPreviousPage, hasNextPage }; + mountComponent({ + propsData: { ...defaultPropsData, pagination }, + }); + + expect(findPagination().exists()).toBe(visible); + }, + ); + + it('pagination emits the correct events', () => { + mountComponent({ + propsData: { ...defaultPropsData, pagination }, + }); + + findPagination().vm.$emit('prev'); + + expect(wrapper.emitted('prev-page')).toEqual([[]]); + + findPagination().vm.$emit('next'); + + expect(wrapper.emitted('next-page')).toEqual([[]]); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js new file mode 100644 index 00000000000..d6d1970cb12 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js @@ -0,0 +1,32 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue'; + +describe('PackageIconAndName', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + + const mountComponent = () => { + wrapper = shallowMount(PackageIconAndName, { + slots: { + default: 'test', + }, + }); + }; + + it('has an icon', () => { + mountComponent(); + + const icon = findIcon(); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('package'); + }); + + it('renders the slot content', () => { + mountComponent(); + + expect(wrapper.text()).toBe('test'); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/package_path_spec.js b/spec/frontend/packages_and_registries/shared/package_path_spec.js new file mode 100644 index 00000000000..93425d4f399 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/package_path_spec.js @@ -0,0 +1,104 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; + +describe('PackagePath', () => { + let wrapper; + + const mountComponent = (propsData = { path: 'foo' }) => { + wrapper = shallowMount(PackagePath, { + propsData, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + const BASE_ICON = 'base-icon'; + const ROOT_LINK = 'root-link'; + const ROOT_CHEVRON = 'root-chevron'; + const ELLIPSIS_ICON = 'ellipsis-icon'; + const ELLIPSIS_CHEVRON = 'ellipsis-chevron'; + const LEAF_LINK = 'leaf-link'; + + const findItem = (name) => wrapper.find(`[data-testid="${name}"]`); + const findTooltip = (w) => getBinding(w.element, 'gl-tooltip'); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + path | rootUrl | shouldExist | shouldNotExist + ${'foo/bar'} | ${'/foo/bar'} | ${[]} | ${[ROOT_CHEVRON, ELLIPSIS_ICON, ELLIPSIS_CHEVRON, LEAF_LINK]} + ${'foo/bar/baz'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK]} | ${[ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} + ${'foo/bar/baz/baz2'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK, ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} | ${[]} + ${'foo/bar/baz/baz2/bar2'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK, ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} | ${[]} + `('given path $path', ({ path, shouldExist, shouldNotExist, rootUrl }) => { + const pathPieces = path.split('/').slice(1); + const hasTooltip = shouldExist.includes(ELLIPSIS_ICON); + + describe('not disabled component', () => { + beforeEach(() => { + mountComponent({ path }); + }); + + it('should have a base icon', () => { + expect(findItem(BASE_ICON).exists()).toBe(true); + }); + + it('should have a root link', () => { + const root = findItem(ROOT_LINK); + expect(root.exists()).toBe(true); + expect(root.attributes('href')).toBe(rootUrl); + }); + + if (hasTooltip) { + it('should have a tooltip', () => { + const tooltip = findTooltip(findItem(ELLIPSIS_ICON)); + expect(tooltip).toBeDefined(); + expect(tooltip.value).toMatchObject({ + title: path, + }); + }); + } + + if (shouldExist.length) { + it.each(shouldExist)(`should have %s`, (element) => { + expect(findItem(element).exists()).toBe(true); + }); + } + + if (shouldNotExist.length) { + it.each(shouldNotExist)(`should not have %s`, (element) => { + expect(findItem(element).exists()).toBe(false); + }); + } + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link should be the last piece of the path', () => { + const leaf = findItem(LEAF_LINK); + expect(leaf.attributes('href')).toBe(`/${path}`); + expect(leaf.text()).toBe(pathPieces[pathPieces.length - 1]); + }); + } + }); + + describe('disabled component', () => { + beforeEach(() => { + mountComponent({ path, disabled: true }); + }); + + it('root link is disabled', () => { + expect(findItem(ROOT_LINK).attributes('disabled')).toBe('true'); + }); + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link is disabled', () => { + expect(findItem(LEAF_LINK).attributes('disabled')).toBe('true'); + }); + } + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/package_tags_spec.js b/spec/frontend/packages_and_registries/shared/package_tags_spec.js new file mode 100644 index 00000000000..33e96c0775e --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/package_tags_spec.js @@ -0,0 +1,107 @@ +import { mount } from '@vue/test-utils'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import { mockTags } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data'; + +describe('PackageTags', () => { + let wrapper; + + function createComponent(tags = [], props = {}) { + const propsData = { + tags, + ...props, + }; + + wrapper = mount(PackageTags, { + propsData, + }); + } + + const tagLabel = () => wrapper.find('[data-testid="tagLabel"]'); + const tagBadges = () => wrapper.findAll('[data-testid="tagBadge"]'); + const moreBadge = () => wrapper.find('[data-testid="moreBadge"]'); + + afterEach(() => { + if (wrapper) wrapper.destroy(); + }); + + describe('tag label', () => { + it('shows the tag label by default', () => { + createComponent(); + + expect(tagLabel().exists()).toBe(true); + }); + + it('hides when hideLabel prop is set to true', () => { + createComponent(mockTags, { hideLabel: true }); + + expect(tagLabel().exists()).toBe(false); + }); + }); + + it('renders the correct number of tags', () => { + createComponent(mockTags.slice(0, 2)); + + expect(tagBadges()).toHaveLength(2); + expect(moreBadge().exists()).toBe(false); + }); + + it('does not render more than the configured tagDisplayLimit', () => { + createComponent(mockTags); + + expect(tagBadges()).toHaveLength(2); + }); + + it('renders the more tags badge if there are more than the configured limit', () => { + createComponent(mockTags); + + expect(tagBadges()).toHaveLength(2); + expect(moreBadge().exists()).toBe(true); + expect(moreBadge().text()).toContain('2'); + }); + + it('renders the configured tagDisplayLimit when set in props', () => { + createComponent(mockTags, { tagDisplayLimit: 1 }); + + expect(tagBadges()).toHaveLength(1); + expect(moreBadge().exists()).toBe(true); + expect(moreBadge().text()).toContain('3'); + }); + + describe('tagBadgeStyle', () => { + const defaultStyle = ['badge', 'badge-info', 'gl-display-none']; + + it('shows tag badge when there is only one', () => { + createComponent([mockTags[0]]); + + const expectedStyle = [...defaultStyle, 'gl-display-flex', 'gl-ml-3']; + + expect(tagBadges().at(0).classes()).toEqual(expect.arrayContaining(expectedStyle)); + }); + + it('shows tag badge for medium or heigher resolutions', () => { + createComponent(mockTags); + + const expectedStyle = [...defaultStyle, 'd-md-flex']; + + expect(tagBadges().at(1).classes()).toEqual(expect.arrayContaining(expectedStyle)); + }); + + it('correctly prepends left and appends right when there is more than one tag', () => { + createComponent(mockTags, { + tagDisplayLimit: 4, + }); + + const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-flex']; + const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'gl-mr-2']; + + const allBadges = tagBadges(); + + expect(allBadges.at(0).classes()).toEqual( + expect.arrayContaining([...expectedStyleWithAppend, 'gl-ml-3']), + ); + expect(allBadges.at(1).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend)); + expect(allBadges.at(2).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend)); + expect(allBadges.at(3).classes()).toEqual(expect.arrayContaining(expectedStyleWithoutAppend)); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js new file mode 100644 index 00000000000..0005162e0bb --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js @@ -0,0 +1,51 @@ +import { mount } from '@vue/test-utils'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; + +describe('PackagesListLoader', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(PackagesListLoader, { + propsData: { + ...props, + }, + }); + }; + + const findDesktopShapes = () => wrapper.find('[data-testid="desktop-loader"]'); + const findMobileShapes = () => wrapper.find('[data-testid="mobile-loader"]'); + + beforeEach(createComponent); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('desktop loader', () => { + it('produces the right loader', () => { + expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20); + }); + + it('has the correct classes', () => { + expect(findDesktopShapes().classes()).toEqual([ + 'gl-display-none', + 'gl-sm-display-flex', + 'gl-flex-direction-column', + ]); + }); + }); + + describe('mobile loader', () => { + it('produces the right loader', () => { + expect(findMobileShapes().findAll('rect[height="170"]')).toHaveLength(5); + }); + + it('has the correct classes', () => { + expect(findMobileShapes().classes()).toEqual([ + 'gl-flex-direction-column', + 'gl-sm-display-none', + ]); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/publish_method_spec.js new file mode 100644 index 00000000000..fa8f8f7641a --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/publish_method_spec.js @@ -0,0 +1,50 @@ +import { shallowMount } from '@vue/test-utils'; +import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; +import { packageList } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data'; + +describe('publish_method', () => { + let wrapper; + + const [packageWithoutPipeline, packageWithPipeline] = packageList; + + const findPipelineRef = () => wrapper.find('[data-testid="pipeline-ref"]'); + const findPipelineSha = () => wrapper.find('[data-testid="pipeline-sha"]'); + const findManualPublish = () => wrapper.find('[data-testid="manually-published"]'); + + const mountComponent = (packageEntity = {}, isGroup = false) => { + wrapper = shallowMount(PublishMethod, { + propsData: { + packageEntity, + isGroup, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders', () => { + mountComponent(packageWithPipeline); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('pipeline information', () => { + it('displays branch and commit when pipeline info exists', () => { + mountComponent(packageWithPipeline); + + expect(findPipelineRef().exists()).toBe(true); + expect(findPipelineSha().exists()).toBe(true); + }); + + it('does not show any pipeline details when no information exists', () => { + mountComponent(packageWithoutPipeline); + + expect(findPipelineRef().exists()).toBe(false); + expect(findPipelineSha().exists()).toBe(false); + expect(findManualPublish().exists()).toBe(true); + expect(findManualPublish().text()).toBe('Manually Published'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/utils_spec.js b/spec/frontend/packages_and_registries/shared/utils_spec.js index bbc8791ca21..962cb2257ce 100644 --- a/spec/frontend/packages_and_registries/shared/utils_spec.js +++ b/spec/frontend/packages_and_registries/shared/utils_spec.js @@ -4,8 +4,12 @@ import { keyValueToFilterToken, searchArrayToFilterTokens, extractFilterAndSorting, + beautifyPath, + getCommitLink, } from '~/packages_and_registries/shared/utils'; +import { packageList } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data'; + describe('Packages And Registries shared utils', () => { describe('getQueryParams', () => { it('returns an object from a query string, with arrays', () => { @@ -56,4 +60,30 @@ describe('Packages And Registries shared utils', () => { }, ); }); + + describe('beautifyPath', () => { + it('returns a string with spaces around /', () => { + expect(beautifyPath('foo/bar')).toBe('foo / bar'); + }); + it('does not fail for empty string', () => { + expect(beautifyPath()).toBe(''); + }); + }); + + describe('getCommitLink', () => { + it('returns a relative link when isGroup is false', () => { + const link = getCommitLink(packageList[0], false); + + expect(link).toContain('../commit'); + }); + + describe('when isGroup is true', () => { + it('returns an absolute link matching project path', () => { + const mavenPackage = packageList[0]; + const link = getCommitLink(mavenPackage, true); + + expect(link).toContain(`/${mavenPackage.project_path}/commit`); + }); + }); + }); }); |