diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
commit | 311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch) | |
tree | 07e7870bca8aed6d61fdcc810731c50d2c40af47 /spec/frontend/packages_and_registries/package_registry | |
parent | 27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff) | |
download | gitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz |
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'spec/frontend/packages_and_registries/package_registry')
12 files changed, 527 insertions, 218 deletions
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap index 451cf743e35..519014bb9cf 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap @@ -19,15 +19,15 @@ exports[`PackageTitle renders with tags 1`] = ` <div class="gl-display-flex gl-flex-direction-column" > - <h1 - class="gl-font-size-h1 gl-mt-3 gl-mb-2" + <h2 + class="gl-font-size-h1 gl-mt-3 gl-mb-0" data-testid="title" > @gitlab-org/package-15 - </h1> + </h2> <div - class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3" > <gl-icon-stub class="gl-mr-3" @@ -117,15 +117,15 @@ exports[`PackageTitle renders without tags 1`] = ` <div class="gl-display-flex gl-flex-direction-column" > - <h1 - class="gl-font-size-h1 gl-mt-3 gl-mb-2" + <h2 + class="gl-font-size-h1 gl-mt-3 gl-mb-0" data-testid="title" > @gitlab-org/package-15 - </h1> + </h2> <div - class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3" > <gl-icon-stub class="gl-mr-3" 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 8f69f943112..c95538546c1 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 @@ -27,6 +27,7 @@ exports[`VersionRow renders 1`] = ` > <span class="gl-truncate" + data-testid="truncate-end-container" title="@gitlab-org/package-15" > <span diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js index 5119512564f..0bea84693f6 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js @@ -16,16 +16,15 @@ import PackageFiles from '~/packages_and_registries/package_registry/components/ import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; +import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; import { FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, - DELETE_PACKAGE_ERROR_MESSAGE, PACKAGE_TYPE_COMPOSER, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, } from '~/packages_and_registries/package_registry/constants'; -import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import { @@ -34,8 +33,6 @@ import { packageVersions, dependencyLinks, emptyPackageDetailsQuery, - packageDestroyMutation, - packageDestroyMutationError, packageFiles, packageDestroyFileMutation, packageDestroyFileMutationError, @@ -64,14 +61,12 @@ describe('PackagesApp', () => { function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), - mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()), fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()), } = {}) { localVue.use(VueApollo); const requestHandlers = [ [getPackageDetails, resolver], - [destroyPackageMutation, mutationResolver], [destroyPackageFileMutation, fileDeleteMutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -82,6 +77,7 @@ describe('PackagesApp', () => { provide, stubs: { PackageTitle, + DeletePackage, GlModal: { template: '<div></div>', methods: { @@ -108,6 +104,7 @@ describe('PackagesApp', () => { const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge); const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); + const findDeletePackage = () => wrapper.findComponent(DeletePackage); afterEach(() => { wrapper.destroy(); @@ -187,14 +184,6 @@ describe('PackagesApp', () => { }); }; - const performDeletePackage = async () => { - await findDeleteButton().trigger('click'); - - findDeleteModal().vm.$emit('primary'); - - await waitForPromises(); - }; - afterEach(() => { Object.defineProperty(document, 'referrer', { value: originalReferrer, @@ -220,7 +209,7 @@ describe('PackagesApp', () => { await waitForPromises(); - await performDeletePackage(); + findDeletePackage().vm.$emit('end'); expect(window.location.replace).toHaveBeenCalledWith( 'projectListUrl?showSuccessDeleteAlert=true', @@ -234,45 +223,13 @@ describe('PackagesApp', () => { await waitForPromises(); - await performDeletePackage(); + findDeletePackage().vm.$emit('end'); expect(window.location.replace).toHaveBeenCalledWith( 'groupListUrl?showSuccessDeleteAlert=true', ); }); }); - - describe('request failure', () => { - it('on global failure it displays an alert', async () => { - createComponent({ mutationResolver: jest.fn().mockRejectedValue() }); - - await waitForPromises(); - - await performDeletePackage(); - - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_ERROR_MESSAGE, - }), - ); - }); - - it('on payload with error it displays an alert', async () => { - createComponent({ - mutationResolver: jest.fn().mockResolvedValue(packageDestroyMutationError()), - }); - - await waitForPromises(); - - await performDeletePackage(); - - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_ERROR_MESSAGE, - }), - ); - }); - }); }); describe('package files', () => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js index b24946c8638..8bb05b00e65 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js @@ -33,12 +33,12 @@ describe('InstallationCommands', () => { }); } - const npmInstallation = () => wrapper.find(NpmInstallation); - const mavenInstallation = () => wrapper.find(MavenInstallation); - const conanInstallation = () => wrapper.find(ConanInstallation); - const nugetInstallation = () => wrapper.find(NugetInstallation); - const pypiInstallation = () => wrapper.find(PypiInstallation); - const composerInstallation = () => wrapper.find(ComposerInstallation); + const npmInstallation = () => wrapper.findComponent(NpmInstallation); + const mavenInstallation = () => wrapper.findComponent(MavenInstallation); + const conanInstallation = () => wrapper.findComponent(ConanInstallation); + const nugetInstallation = () => wrapper.findComponent(NugetInstallation); + const pypiInstallation = () => wrapper.findComponent(PypiInstallation); + const composerInstallation = () => wrapper.findComponent(ComposerInstallation); afterEach(() => { wrapper.destroy(); @@ -57,7 +57,7 @@ describe('InstallationCommands', () => { it(`${packageEntity.packageType} instructions exist`, () => { createComponent({ packageEntity }); - expect(selector()).toExist(); + expect(selector().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js new file mode 100644 index 00000000000..5de30829fa5 --- /dev/null +++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js @@ -0,0 +1,160 @@ +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import createFlash from '~/flash'; +import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; + +import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; +import { + packageDestroyMutation, + packageDestroyMutationError, + packagesListQuery, +} from '../../mock_data'; + +jest.mock('~/flash'); + +const localVue = createLocalVue(); + +describe('DeletePackage', () => { + let wrapper; + let apolloProvider; + let resolver; + let mutationResolver; + + const eventPayload = { id: '1' }; + + function createComponent(propsData = {}) { + localVue.use(VueApollo); + + const requestHandlers = [ + [getPackagesQuery, resolver], + [destroyPackageMutation, mutationResolver], + ]; + apolloProvider = createMockApollo(requestHandlers); + + wrapper = shallowMountExtended(DeletePackage, { + propsData, + localVue, + apolloProvider, + scopedSlots: { + default(props) { + return this.$createElement('button', { + attrs: { + 'data-testid': 'trigger-button', + }, + on: { + click: props.deletePackage, + }, + }); + }, + }, + }); + } + + const findButton = () => wrapper.findByTestId('trigger-button'); + + const clickOnButtonAndWait = (payload) => { + findButton().trigger('click', payload); + return waitForPromises(); + }; + + beforeEach(() => { + resolver = jest.fn().mockResolvedValue(packagesListQuery()); + mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('binds deletePackage method to the default slot', () => { + createComponent(); + + findButton().trigger('click'); + + expect(wrapper.emitted('start')).toEqual([[]]); + }); + + it('calls apollo mutation', async () => { + createComponent(); + + await clickOnButtonAndWait(eventPayload); + + expect(mutationResolver).toHaveBeenCalledWith(eventPayload); + }); + + it('passes refetchQueries to apollo mutate', async () => { + const variables = { isGroupPage: true }; + createComponent({ + refetchQueries: [{ query: getPackagesQuery, variables }], + }); + + await clickOnButtonAndWait(eventPayload); + + expect(mutationResolver).toHaveBeenCalledWith(eventPayload); + expect(resolver).toHaveBeenCalledWith(variables); + }); + + describe('on mutation success', () => { + it('emits end event', async () => { + createComponent(); + + await clickOnButtonAndWait(eventPayload); + + expect(wrapper.emitted('end')).toEqual([[]]); + }); + + it('does not call createFlash', async () => { + createComponent(); + + await clickOnButtonAndWait(eventPayload); + + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('calls createFlash with the success message when showSuccessAlert is true', async () => { + createComponent({ showSuccessAlert: true }); + + await clickOnButtonAndWait(eventPayload); + + expect(createFlash).toHaveBeenCalledWith({ + message: DeletePackage.i18n.successMessage, + type: 'success', + }); + }); + }); + + describe.each` + errorType | mutationResolverResponse + ${'connectionError'} | ${jest.fn().mockRejectedValue()} + ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())} + `('on mutation $errorType', ({ mutationResolverResponse }) => { + beforeEach(() => { + mutationResolver = mutationResolverResponse; + }); + + it('emits end event', async () => { + createComponent(); + + await clickOnButtonAndWait(eventPayload); + + expect(wrapper.emitted('end')).toEqual([[]]); + }); + + it('calls createFlash with the error message', async () => { + createComponent({ showSuccessAlert: true }); + + await clickOnButtonAndWait(eventPayload); + + expect(createFlash).toHaveBeenCalledWith({ + message: DeletePackage.i18n.errorMessage, + type: 'warning', + captureError: true, + error: expect.any(Error), + }); + }); + }); +}); 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/components/list/__snapshots__/app_spec.js.snap index 1b556be5873..5af75868084 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap @@ -8,5 +8,62 @@ exports[`PackagesListApp renders 1`] = ` /> <package-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="emptyListIllustration" + /> + </div> + </div> + + <div + class="col-12" + > + <div + class="text-content gl-mx-auto gl-my-0 gl-p-5" + > + <h1 + class="h4" + > + There are no packages yet + </h1> + + <p> + Learn how to + <b-link-stub + class="gl-link" + event="click" + href="emptyListHelpUrl" + 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/package_registry/components/list/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js index 3958cdf21bb..ad848f367e0 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js @@ -2,22 +2,25 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +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 PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue'; +import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; +import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; import { PROJECT_RESOURCE_TYPE, GROUP_RESOURCE_TYPE, - LIST_QUERY_DEBOUNCE_TIME, + GRAPHQL_PAGE_SIZE, } from '~/packages_and_registries/package_registry/constants'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; -import { packagesListQuery } from '../../mock_data'; +import { packagesListQuery, packageData, pagination } from '../../mock_data'; jest.mock('~/lib/utils/common_utils'); jest.mock('~/flash'); @@ -39,11 +42,20 @@ describe('PackagesListApp', () => { const PackageList = { name: 'package-list', template: '<div><slot name="empty-state"></slot></div>', + props: OriginalPackageList.props, }; const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' }; + const searchPayload = { + sort: 'VERSION_DESC', + filters: { packageName: 'foo', packageType: 'CONAN' }, + }; + const findPackageTitle = () => wrapper.findComponent(PackageTitle); const findSearch = () => wrapper.findComponent(PackageSearch); + const findListComponent = () => wrapper.findComponent(PackageList); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findDeletePackage = () => wrapper.findComponent(DeletePackage); const mountComponent = ({ resolver = jest.fn().mockResolvedValue(packagesListQuery()), @@ -61,9 +73,10 @@ describe('PackagesListApp', () => { stubs: { GlEmptyState, GlLoadingIcon, - PackageList, GlSprintf, GlLink, + PackageList, + DeletePackage, }, }); }; @@ -72,15 +85,24 @@ describe('PackagesListApp', () => { wrapper.destroy(); }); - const waitForDebouncedApollo = () => { - jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME); + const waitForFirstRequest = () => { + // emit a search update so the query is executed + findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] }); return waitForPromises(); }; + it('does not execute the query without sort being set', () => { + const resolver = jest.fn().mockResolvedValue(packagesListQuery()); + + mountComponent({ resolver }); + + expect(resolver).not.toHaveBeenCalled(); + }); + it('renders', async () => { mountComponent(); - await waitForDebouncedApollo(); + await waitForFirstRequest(); expect(wrapper.element).toMatchSnapshot(); }); @@ -88,7 +110,7 @@ describe('PackagesListApp', () => { it('has a package title', async () => { mountComponent(); - await waitForDebouncedApollo(); + await waitForFirstRequest(); expect(findPackageTitle().exists()).toBe(true); expect(findPackageTitle().props('count')).toBe(2); @@ -105,25 +127,54 @@ describe('PackagesListApp', () => { const resolver = jest.fn().mockResolvedValue(packagesListQuery()); mountComponent({ resolver }); - const payload = { - sort: 'VERSION_DESC', - filters: { packageName: 'foo', packageType: 'CONAN' }, - }; - - findSearch().vm.$emit('update', payload); + findSearch().vm.$emit('update', searchPayload); - await waitForDebouncedApollo(); - jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME); + await waitForPromises(); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ - groupSort: payload.sort, - ...payload.filters, + groupSort: searchPayload.sort, + ...searchPayload.filters, }), ); }); }); + describe('list component', () => { + let resolver; + + beforeEach(() => { + resolver = jest.fn().mockResolvedValue(packagesListQuery()); + mountComponent({ resolver }); + + return waitForFirstRequest(); + }); + + it('exists and has the right props', () => { + expect(findListComponent().props()).toMatchObject({ + list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]), + isLoading: false, + pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }), + }); + }); + + it('when list emits next-page fetches the next set of records', () => { + findListComponent().vm.$emit('next-page'); + + expect(resolver).toHaveBeenCalledWith( + expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }), + ); + }); + + it('when list emits prev-page fetches the prev set of records', () => { + findListComponent().vm.$emit('prev-page'); + + expect(resolver).toHaveBeenCalledWith( + expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }), + ); + }); + }); + describe.each` type | sortType ${PROJECT_RESOURCE_TYPE} | ${'sort'} @@ -136,9 +187,9 @@ describe('PackagesListApp', () => { beforeEach(() => { provide = { ...defaultProvide, isGroupPage }; - resolver = jest.fn().mockResolvedValue(packagesListQuery(type)); + resolver = jest.fn().mockResolvedValue(packagesListQuery({ type })); mountComponent({ provide, resolver }); - return waitForDebouncedApollo(); + return waitForFirstRequest(); }); it('succeeds', () => { @@ -147,8 +198,85 @@ describe('PackagesListApp', () => { it('calls the resolver with the right parameters', () => { expect(resolver).toHaveBeenCalledWith( - expect.objectContaining({ isGroupPage, [sortType]: '' }), + expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }), ); }); }); + + describe('empty state', () => { + beforeEach(() => { + const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } })); + mountComponent({ resolver }); + + return waitForFirstRequest(); + }); + it('generate the correct empty list link', () => { + const link = findListComponent().findComponent(GlLink); + + expect(link.attributes('href')).toBe(defaultProvide.emptyListHelpUrl); + 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); + }); + }); + + describe('filter without results', () => { + beforeEach(async () => { + mountComponent(); + + await waitForFirstRequest(); + + findSearch().vm.$emit('update', searchPayload); + + return nextTick(); + }); + + it('should show specific empty message', () => { + expect(findEmptyState().text()).toContain(PackageListApp.i18n.noResultsTitle); + expect(findEmptyState().text()).toContain(PackageListApp.i18n.widenFilters); + }); + }); + + describe('delete package', () => { + it('exists and has the correct props', async () => { + mountComponent(); + + await waitForFirstRequest(); + + expect(findDeletePackage().props()).toMatchObject({ + refetchQueries: [{ query: getPackagesQuery, variables: {} }], + showSuccessAlert: true, + }); + }); + + it('deletePackage is bound to package-list package:delete event', async () => { + mountComponent(); + + await waitForFirstRequest(); + + findListComponent().vm.$emit('package:delete', { id: 1 }); + + expect(findDeletePackage().emitted('start')).toEqual([[]]); + }); + + it('start and end event set loading correctly', async () => { + mountComponent(); + + await waitForFirstRequest(); + + findDeletePackage().vm.$emit('start'); + + await nextTick(); + + expect(findListComponent().props('isLoading')).toBe(true); + + findDeletePackage().vm.$emit('end'); + + await nextTick(); + + expect(findListComponent().props('isLoading')).toBe(false); + }); + }); }); 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 b624e66482d..de4e9c8ae5b 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,93 +1,86 @@ -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 { packageList } from 'jest/packages/mock_data'; +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 { TrackingActions } from '~/packages/shared/constants'; -import * as SharedUtils from '~/packages/shared/utils'; +import { + DELETE_PACKAGE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_TRACKING_ACTION, +} from '~/packages_and_registries/package_registry/constants'; import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; import Tracking from '~/tracking'; - -const localVue = createLocalVue(); -localVue.use(Vuex); +import { packageData } from '../../mock_data'; describe('packages_list', () => { let wrapper; - let store; + + const firstPackage = packageData(); + const secondPackage = { + ...packageData(), + id: 'gid://gitlab/Packages::Package/112', + name: 'second-package', + }; + + const defaultProps = { + list: [firstPackage, secondPackage], + isLoading: false, + pageInfo: {}, + }; const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' }; + const GlModalStub = { + name: GlModal.name, + template: '<div><slot></slot></div>', + methods: { show: jest.fn() }, + }; - 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, + const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader); + const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination); + const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub); + const findEmptySlot = () => wrapper.findComponent(EmptySlotStub); + const findPackagesListRow = () => wrapper.findComponent(PackagesListRow); + + const mountComponent = (props) => { + wrapper = shallowMountExtended(PackagesList, { + propsData: { + ...defaultProps, + ...props, }, - sorting: { - orderBy: 'version', - sort: 'desc', + stubs: { + GlModal: GlModalStub, + GlSprintf, }, - }; - store = new Vuex.Store({ - state, - getters: { - getList: () => packages, + slots: { + 'empty-state': EmptySlotStub, }, }); - 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, - }); - }; + beforeEach(() => { + GlModalStub.methods.show.mockReset(); + }); afterEach(() => { wrapper.destroy(); - wrapper = null; }); describe('when is loading', () => { beforeEach(() => { - mountComponent({ - packages: [], - isLoading: true, - }); + mountComponent({ isLoading: true }); }); - it('shows skeleton loader when loading', () => { + it('shows skeleton loader', () => { expect(findPackagesListLoader().exists()).toBe(true); }); + + it('does not show the rows', () => { + expect(findPackagesListRow().exists()).toBe(false); + }); + + it('does not show the pagination', () => { + expect(findPackageListPagination().exists()).toBe(false); + }); }); describe('when is not loading', () => { @@ -95,74 +88,61 @@ describe('packages_list', () => { mountComponent(); }); - it('does not show skeleton loader when not loading', () => { + it('does not show skeleton loader', () => { expect(findPackagesListLoader().exists()).toBe(false); }); - }); - describe('layout', () => { - beforeEach(() => { - mountComponent(); + it('shows the rows', () => { + expect(findPackagesListRow().exists()).toBe(true); }); + }); + describe('layout', () => { it('contains a pagination component', () => { - const sorting = findPackageListPagination(); - expect(sorting.exists()).toBe(true); + mountComponent({ pageInfo: { hasPreviousPage: true } }); + + expect(findPackageListPagination().exists()).toBe(true); }); it('contains a modal component', () => { - const sorting = findPackageListDeleteModal(); - expect(sorting.exists()).toBe(true); + mountComponent(); + + expect(findPackageListDeleteModal().exists()).toBe(true); }); }); describe('when the user can destroy the package', () => { beforeEach(() => { mountComponent(); + findPackagesListRow().vm.$emit('packageToDelete', firstPackage); + return nextTick(); }); - it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => { - const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show'); - const item = last(wrapper.vm.list); + it('deleting a package opens the modal', () => { + expect(findPackageListDeleteModal().text()).toContain(firstPackage.name); + }); - findPackagesListRow().vm.$emit('packageToDelete', item); + it('confirming on the modal emits package:delete', async () => { + findPackageListDeleteModal().vm.$emit('ok'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.itemToBeDeleted).toEqual(item); - expect(mockModalShow).toHaveBeenCalled(); - }); - }); + await nextTick(); - it('deleteItemConfirmation resets itemToBeDeleted', () => { - wrapper.setData({ itemToBeDeleted: 1 }); - wrapper.vm.deleteItemConfirmation(); - expect(wrapper.vm.itemToBeDeleted).toEqual(null); + expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]); }); - 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('closing the modal resets itemToBeDeleted', async () => { + // triggering the v-model + findPackageListDeleteModal().vm.$emit('input', false); - it('deleteItemCanceled resets itemToBeDeleted', () => { - wrapper.setData({ itemToBeDeleted: 1 }); - wrapper.vm.deleteItemCanceled(); - expect(wrapper.vm.itemToBeDeleted).toEqual(null); + await nextTick(); + + expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name); }); }); describe('when the list is empty', () => { beforeEach(() => { - mountComponent({ - packages: [], - slots: { - 'empty-state': EmptySlotStub, - }, - }); + mountComponent({ list: [] }); }); it('show the empty slot', () => { @@ -171,45 +151,59 @@ describe('packages_list', () => { }); }); - describe('pagination component', () => { - let pagination; - let modelEvent; - + describe('pagination ', () => { beforeEach(() => { - mountComponent(); - pagination = findPackageListPagination(); - // retrieve the event used by v-model, a more sturdy approach than hardcoding it - modelEvent = pagination.vm.$options.model.event; + mountComponent({ pageInfo: { hasPreviousPage: true } }); }); - it('emits page:changed events when the page changes', () => { - pagination.vm.$emit(modelEvent, 2); - expect(wrapper.emitted('page:changed')).toEqual([[2]]); + it('emits prev-page events when the prev event is fired', () => { + findPackageListPagination().vm.$emit('prev'); + + expect(wrapper.emitted('prev-page')).toEqual([[]]); + }); + + it('emits next-page events when the next event is fired', () => { + findPackageListPagination().vm.$emit('next'); + + expect(wrapper.emitted('next-page')).toEqual([[]]); }); }); describe('tracking', () => { let eventSpy; - let utilSpy; - const category = 'foo'; + const category = 'UI::NpmPackages'; beforeEach(() => { - mountComponent(); eventSpy = jest.spyOn(Tracking, 'event'); - utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category); - wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } }); + mountComponent(); + findPackagesListRow().vm.$emit('packageToDelete', firstPackage); + return nextTick(); }); - it('tracking category calls packageTypeToTrackCategory', () => { - expect(wrapper.vm.tracking.category).toBe(category); - expect(utilSpy).toHaveBeenCalledWith('conan'); + it('requesting the delete tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + category, + REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + expect.any(Object), + ); + }); + + it('confirming delete tracks the right action', () => { + findPackageListDeleteModal().vm.$emit('ok'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + DELETE_PACKAGE_TRACKING_ACTION, + expect.any(Object), + ); }); - it('deleteItemConfirmation calls event', () => { - wrapper.vm.deleteItemConfirmation(); + it('canceling delete tracks the right action', () => { + findPackageListDeleteModal().vm.$emit('cancel'); + expect(eventSpy).toHaveBeenCalledWith( category, - TrackingActions.DELETE_PACKAGE, + CANCEL_DELETE_PACKAGE_TRACKING_ACTION, expect.any(Object), ); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js index e65b2a6f320..bed7a07ff36 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js @@ -1,6 +1,6 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { sortableFields } from '~/packages/list/utils'; +import { sortableFields } from '~/packages_and_registries/package_registry/utils'; import component from '~/packages_and_registries/package_registry/components/list/package_search.vue'; import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js index 3fa96ce1d29..e992ba12faa 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js @@ -1,5 +1,4 @@ import { shallowMount } from '@vue/test-utils'; -import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants'; import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; @@ -37,8 +36,8 @@ describe('PackageTitle', () => { mountComponent(); expect(findTitleArea().props()).toMatchObject({ - title: LIST_TITLE_TEXT, - infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }], + title: PackageTitle.i18n.LIST_TITLE_TEXT, + infoMessages: [{ text: PackageTitle.i18n.LIST_INTRO_TEXT, link: 'foo' }], }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js index b0cbe34f0b9..26b2f3b359f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js @@ -1,7 +1,7 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import component from '~/packages/list/components/tokens/package_type_token.vue'; -import { PACKAGE_TYPES } from '~/packages/list/constants'; +import component from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue'; +import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/constants'; describe('packages_filter', () => { let wrapper; @@ -41,8 +41,8 @@ describe('packages_filter', () => { (packageType, index) => { mountComponent(); const item = findFilteredSearchSuggestions().at(index); - expect(item.text()).toBe(packageType.title); - expect(item.props('value')).toBe(packageType.type); + expect(item.text()).toBe(packageType); + expect(item.props('value')).toBe(packageType); }, ); }); 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 70fc096fa44..bacc748db81 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -1,3 +1,5 @@ +import capitalize from 'lodash/capitalize'; + export const packageTags = () => [ { id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' }, { id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' }, @@ -156,6 +158,15 @@ export const nugetMetadata = () => ({ projectUrl: 'projectUrl', }); +export const pagination = (extend) => ({ + endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ', + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0', + __typename: 'PageInfo', + ...extend, +}); + export const packageDetailsQuery = (extendPackage) => ({ data: { package: { @@ -256,7 +267,7 @@ export const packageDestroyFileMutationError = () => ({ ], }); -export const packagesListQuery = (type = 'group') => ({ +export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({ data: { [type]: { packages: { @@ -277,9 +288,11 @@ export const packagesListQuery = (type = 'group') => ({ pipelines: { nodes: [] }, }, ], + pageInfo: pagination(extendPagination), __typename: 'PackageConnection', }, - __typename: 'Group', + ...extend, + __typename: capitalize(type), }, }, }); |