diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/frontend/packages_and_registries | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) | |
download | gitlab-ce-71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e.tar.gz |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/frontend/packages_and_registries')
16 files changed, 448 insertions, 295 deletions
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 329cc15df97..601f8abd34d 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -17,6 +17,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants'; import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status'; import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; @@ -92,7 +93,7 @@ describe('DependencyProxyApp', () => { window.gon = { ...dummyGon }; mock = new MockAdapter(axios); - mock.onDelete(expectedUrl).reply(202, {}); + mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {}); }); afterEach(() => { 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 index a33528d2d91..801cde8582e 100644 --- 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 @@ -30,6 +30,7 @@ exports[`packages_list_app renders 1`] = ` <div class="gl-max-w-full gl-m-auto" + data-testid="gl-empty-state-content" > <div class="gl-mx-auto gl-my-0 gl-p-5" 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 index 36417eaf793..2c185e040f4 100644 --- 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 @@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; import { createAlert } from '~/flash'; +import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; 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'; @@ -182,7 +183,7 @@ describe('Actions Package list store', () => { }, }; it('should perform a delete operation on _links.delete_api_path', () => { - mock.onDelete(payload._links.delete_api_path).replyOnce(200); + mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_OK); Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' }); return testAction( @@ -198,7 +199,7 @@ describe('Actions Package list store', () => { }); it('should stop the loading and call create flash on api error', async () => { - mock.onDelete(payload._links.delete_api_path).replyOnce(400); + mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST); await testAction( actions.requestDeletePackage, payload, 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 index 91824dee5b0..08e2de6c18f 100644 --- 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 @@ -101,18 +101,6 @@ exports[`packages_list_row renders 1`] = ` </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/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 deleted file mode 100644 index bdd0fe3ad9e..00000000000 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap +++ /dev/null @@ -1,104 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VersionRow 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" -> - <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" - href="243" - > - <span - class="gl-truncate" - data-testid="truncate-end-container" - title="@gitlab-org/package-15" - > - <span - class="gl-truncate-end" - > - @gitlab-org/package-15 - </span> - </span> - </gl-link-stub> - - <package-tags-stub - class="gl-ml-3" - hidelabel="true" - tagdisplaylimit="1" - tags="[object Object],[object Object],[object Object]" - /> - </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" - > - - 1.0.1 - - </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> - Created - <time-ago-tooltip-stub - cssclass="" - time="2021-08-10T09:33:54Z" - tooltipplacement="top" - /> - </span> - </div> - </div> - </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/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js index 20a459e2c1a..27c0ab96cfc 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js @@ -1,8 +1,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; +import Tracking from '~/tracking'; +import { + CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, +} from '~/packages_and_registries/package_registry/constants'; import { packageData } from '../../mock_data'; describe('PackageVersionsList', () => { @@ -24,6 +32,7 @@ describe('PackageVersionsList', () => { findRegistryList: () => wrapper.findComponent(RegistryList), findEmptySlot: () => wrapper.findComponent(EmptySlotStub), findListRow: () => wrapper.findAllComponents(VersionRow), + findDeletePackagesModal: () => wrapper.findComponent(DeleteModal), }; const mountComponent = (props) => { wrapper = shallowMountExtended(PackageVersionsList, { @@ -35,6 +44,11 @@ describe('PackageVersionsList', () => { }, stubs: { RegistryList, + DeleteModal: stubComponent(DeleteModal, { + methods: { + show: jest.fn(), + }, + }), }, slots: { 'empty-state': EmptySlotStub, @@ -144,4 +158,80 @@ describe('PackageVersionsList', () => { expect(wrapper.emitted('next-page')).toHaveLength(1); }); }); + + describe('when the user can bulk destroy versions', () => { + let eventSpy; + const { findDeletePackagesModal, findRegistryList } = uiElements; + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + mountComponent({ canDestroy: true }); + }); + + it('binds the right props', () => { + expect(uiElements.findRegistryList().props()).toMatchObject({ + items: packageList, + pagination: {}, + isLoading: false, + hiddenDelete: false, + title: '2 versions', + }); + }); + + describe('upon deletion', () => { + beforeEach(() => { + findRegistryList().vm.$emit('delete', packageList); + }); + + it('passes itemsToBeDeleted to the modal', () => { + expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList); + expect(wrapper.emitted('delete')).toBeUndefined(); + }); + + it('requesting delete tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + undefined, + REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + expect.any(Object), + ); + }); + + describe('when modal confirms', () => { + beforeEach(() => { + findDeletePackagesModal().vm.$emit('confirm'); + }); + + it('emits delete event', () => { + expect(wrapper.emitted('delete')[0]).toEqual([packageList]); + }); + + it('tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + undefined, + DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + + it.each(['confirm', 'cancel'])( + 'resets itemsToBeDeleted when modal emits %s', + async (event) => { + await findDeletePackagesModal().vm.$emit(event); + + expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0); + }, + ); + + it('canceling delete tracks the right action', () => { + findDeletePackagesModal().vm.$emit('cancel'); + + expect(eventSpy).toHaveBeenCalledWith( + undefined, + CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + }); }); 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 faeca76d746..67340822fa5 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,11 +1,13 @@ -import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import ListItem from '~/vue_shared/components/registry/list_item.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'; +import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { packageVersions } from '../../mock_data'; @@ -19,17 +21,23 @@ describe('VersionRow', () => { const findPackageTags = () => wrapper.findComponent(PackageTags); const findPublishMethod = () => wrapper.findComponent(PublishMethod); const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); + const findPackageName = () => wrapper.findComponent(GlTruncate); + const findWarningIcon = () => wrapper.findComponent(GlIcon); + const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); - function createComponent(packageEntity = packageVersion) { + function createComponent({ packageEntity = packageVersion, selected = false } = {}) { wrapper = shallowMountExtended(VersionRow, { propsData: { packageEntity, + selected, }, stubs: { - ListItem, GlSprintf, GlTruncate, }, + directives: { + GlTooltip: createMockDirective(), + }, }); } @@ -37,16 +45,15 @@ describe('VersionRow', () => { wrapper.destroy(); }); - it('renders', () => { + it('has a link to the version detail', () => { createComponent(); - expect(wrapper.element).toMatchSnapshot(); + expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`); }); - it('has a link to the version detail', () => { + it('lists the package name', () => { createComponent(); - expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`); expect(findLink().text()).toBe(packageVersion.name); }); @@ -73,17 +80,89 @@ describe('VersionRow', () => { expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt); }); - describe('disabled status', () => { - it('disables the list item', () => { - createComponent({ ...packageVersion, status: 'something' }); + describe('left action template', () => { + it('does not render checkbox if not permitted', () => { + createComponent({ packageEntity: { ...packageVersion, canDestroy: false } }); + + expect(findBulkDeleteAction().exists()).toBe(false); + }); + + it('renders checkbox', () => { + createComponent(); + + expect(findBulkDeleteAction().exists()).toBe(true); + expect(findBulkDeleteAction().attributes('checked')).toBeUndefined(); + }); + + it('emits select when checked', () => { + createComponent(); + + findBulkDeleteAction().vm.$emit('change'); + + expect(wrapper.emitted('select')).toHaveLength(1); + }); + + it('renders checkbox in selected state if selected', () => { + createComponent({ + selected: true, + }); + + expect(findBulkDeleteAction().attributes('checked')).toBe('true'); + expect(findListItem().props('selected')).toBe(true); + }); + }); + + describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { + beforeEach(() => { + createComponent({ + packageEntity: { + ...packageVersion, + status: PACKAGE_ERROR_STATUS, + _links: { + webPath: null, + }, + }, + }); + }); + + it('lists the package name', () => { + expect(findPackageName().props('text')).toBe('@gitlab-org/package-15'); + }); + + it('does not have a link to navigate to the details page', () => { + expect(findLink().exists()).toBe(false); + }); + + it('has a warning icon', () => { + const icon = findWarningIcon(); + const tooltip = getBinding(icon.element, 'gl-tooltip'); + expect(icon.props('name')).toBe('warning'); + expect(icon.props('ariaLabel')).toBe('Warning'); + expect(tooltip.value).toMatchObject({ + title: 'Invalid Package: failed metadata extraction', + }); + }); + }); - expect(findListItem().props('disabled')).toBe(true); + describe('disabled status', () => { + beforeEach(() => { + createComponent({ + packageEntity: { + ...packageVersion, + status: 'something', + _links: { + webPath: null, + }, + }, + }); }); - it('disables the link', () => { - createComponent({ ...packageVersion, status: 'something' }); + it('lists the package name', () => { + expect(findPackageName().props('text')).toBe('@gitlab-org/package-15'); + }); - expect(findLink().attributes('disabled')).toBe('true'); + it('does not have a link to navigate to the details page', () => { + expect(findLink().exists()).toBe(false); }); }); }); 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_packages_spec.js index 93c2196b210..689b53fa2a4 100644 --- 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_packages_spec.js @@ -4,36 +4,38 @@ import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; -import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; +import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; -import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; import { - packageDestroyMutation, - packageDestroyMutationError, + packagesDestroyMutation, + packagesDestroyMutationError, packagesListQuery, } from '../../mock_data'; jest.mock('~/flash'); -describe('DeletePackage', () => { +describe('DeletePackages', () => { let wrapper; let apolloProvider; let resolver; let mutationResolver; - const eventPayload = { id: '1' }; + const eventPayload = [{ id: '1' }]; + const eventPayloadMultiple = [{ id: '1' }, { id: '2' }]; + const mutationPayload = { ids: ['1'] }; function createComponent(propsData = {}) { Vue.use(VueApollo); const requestHandlers = [ [getPackagesQuery, resolver], - [destroyPackageMutation, mutationResolver], + [destroyPackagesMutation, mutationResolver], ]; apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(DeletePackage, { + wrapper = shallowMountExtended(DeletePackages, { propsData, apolloProvider, scopedSlots: { @@ -43,7 +45,9 @@ describe('DeletePackage', () => { 'data-testid': 'trigger-button', }, on: { - click: props.deletePackage, + click: (payload) => { + return props.deletePackages(payload[0]); + }, }, }); }, @@ -54,23 +58,23 @@ describe('DeletePackage', () => { const findButton = () => wrapper.findByTestId('trigger-button'); const clickOnButtonAndWait = (payload) => { - findButton().trigger('click', payload); + findButton().trigger('click', [payload]); return waitForPromises(); }; beforeEach(() => { resolver = jest.fn().mockResolvedValue(packagesListQuery()); - mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()); + mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation()); }); afterEach(() => { wrapper.destroy(); }); - it('binds deletePackage method to the default slot', () => { + it('binds deletePackages method to the default slot', () => { createComponent(); - findButton().trigger('click'); + findButton().trigger('click', eventPayload); expect(wrapper.emitted('start')).toEqual([[]]); }); @@ -80,7 +84,7 @@ describe('DeletePackage', () => { await clickOnButtonAndWait(eventPayload); - expect(mutationResolver).toHaveBeenCalledWith(eventPayload); + expect(mutationResolver).toHaveBeenCalledWith(mutationPayload); }); it('passes refetchQueries to apollo mutate', async () => { @@ -91,10 +95,20 @@ describe('DeletePackage', () => { await clickOnButtonAndWait(eventPayload); - expect(mutationResolver).toHaveBeenCalledWith(eventPayload); + expect(mutationResolver).toHaveBeenCalledWith(mutationPayload); expect(resolver).toHaveBeenCalledWith(variables); }); + describe('when payload contains multiple packages', () => { + it('calls apollo mutation with different payload', async () => { + createComponent(); + + await clickOnButtonAndWait(eventPayloadMultiple); + + expect(mutationResolver).toHaveBeenCalledWith({ ids: ['1', '2'] }); + }); + }); + describe('on mutation success', () => { it('emits end event', async () => { createComponent(); @@ -118,16 +132,29 @@ describe('DeletePackage', () => { await clickOnButtonAndWait(eventPayload); expect(createAlert).toHaveBeenCalledWith({ - message: DeletePackage.i18n.successMessage, + message: DeletePackages.i18n.successMessage, variant: VARIANT_SUCCESS, }); }); + + describe('when payload contains multiple packages', () => { + it('calls createAlert with success message when showSuccessAlert is true', async () => { + createComponent({ showSuccessAlert: true }); + + await clickOnButtonAndWait(eventPayloadMultiple); + + expect(createAlert).toHaveBeenCalledWith({ + message: DeletePackages.i18n.successMessageMultiple, + variant: VARIANT_SUCCESS, + }); + }); + }); }); describe.each` errorType | mutationResolverResponse ${'connectionError'} | ${jest.fn().mockRejectedValue()} - ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())} + ${'localError'} | ${jest.fn().mockResolvedValue(packagesDestroyMutationError())} `('on mutation $errorType', ({ mutationResolverResponse }) => { beforeEach(() => { mutationResolver = mutationResolverResponse; @@ -147,11 +174,26 @@ describe('DeletePackage', () => { await clickOnButtonAndWait(eventPayload); expect(createAlert).toHaveBeenCalledWith({ - message: DeletePackage.i18n.errorMessage, + message: DeletePackages.i18n.errorMessage, variant: VARIANT_WARNING, captureError: true, error: expect.any(Error), }); }); + + describe('when payload contains multiple packages', () => { + it('calls createAlert with error message', async () => { + createComponent({ showSuccessAlert: true }); + + await clickOnButtonAndWait(eventPayloadMultiple); + + expect(createAlert).toHaveBeenCalledWith({ + message: DeletePackages.i18n.errorMessageMultiple, + variant: VARIANT_WARNING, + captureError: true, + error: expect.any(Error), + }); + }); + }); }); }); 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 a7de751aadd..ec8e77fa923 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 @@ -54,12 +54,15 @@ exports[`packages_list_row renders 1`] = ` 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" + class="gl-display-flex gl-align-items-center" data-testid="left-secondary-infos" > - <span> - 1.0.0 - </span> + <gl-truncate-stub + class="gl-max-w-15 gl-md-max-w-26" + position="end" + text="1.0.0" + withtooltip="true" + /> <!----> @@ -135,18 +138,6 @@ exports[`packages_list_row renders 1`] = ` </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/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 bb04701a8b7..2a78cfb13f9 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 @@ -43,9 +43,11 @@ describe('packages_list_row', () => { const findPackageLink = () => wrapper.findByTestId('details-link'); const findWarningIcon = () => wrapper.findByTestId('warning-icon'); const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos'); + const findPackageVersion = () => findLeftSecondaryInfos().findComponent(GlTruncate); const findPublishMethod = () => wrapper.findComponent(PublishMethod); const findCreatedDateText = () => wrapper.findByTestId('created-date'); const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip); + const findListItem = () => wrapper.findComponent(ListItem); const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); const findPackageName = () => wrapper.findComponent(GlTruncate); @@ -83,22 +85,13 @@ describe('packages_list_row', () => { mountComponent(); expect(findPackageLink().props()).toMatchObject({ - event: 'click', to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } }, }); }); - it('does not have a link to navigate to the details page', () => { - mountComponent({ - packageEntity: { - ...packageWithoutTags, - _links: { - webPath: null, - }, - }, - }); + it('lists the package name', () => { + mountComponent(); - expect(findPackageLink().exists()).toBe(false); expect(findPackageName().props()).toMatchObject({ text: '@gitlab-org/package-15', }); @@ -155,11 +148,25 @@ describe('packages_list_row', () => { describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { beforeEach(() => { - mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } }); + mountComponent({ + packageEntity: { + ...packageWithoutTags, + status: PACKAGE_ERROR_STATUS, + _links: { + webPath: null, + }, + }, + }); }); - it('details link is disabled', () => { - expect(findPackageLink().props('event')).toBe(''); + it('lists the package name', () => { + expect(findPackageName().props()).toMatchObject({ + text: '@gitlab-org/package-15', + }); + }); + + it('does not have a link to navigate to the details page', () => { + expect(findPackageLink().exists()).toBe(false); }); it('has a warning icon', () => { @@ -206,6 +213,9 @@ describe('packages_list_row', () => { }); expect(findBulkDeleteAction().attributes('checked')).toBe('true'); + expect(findListItem().props()).toMatchObject({ + selected: true, + }); }); }); @@ -213,7 +223,10 @@ describe('packages_list_row', () => { it('has the package version', () => { mountComponent(); - expect(findLeftSecondaryInfos().text()).toContain(packageWithoutTags.version); + expect(findPackageVersion().props()).toMatchObject({ + text: packageWithoutTags.version, + withTooltip: true, + }); }); it('if the pipeline exists show the author message', () => { 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 5e9cb8fbb0b..610640e0ca3 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 @@ -167,8 +167,8 @@ describe('packages_list', () => { findPackageListDeleteModal().vm.$emit('ok'); }); - it('emits package:delete when modal confirms', () => { - expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]); + it('emits delete when modal confirms', () => { + expect(wrapper.emitted('delete')[0][0]).toEqual([firstPackage]); }); it('tracks the right 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 9e9e08bc196..d897be1f344 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -97,14 +97,22 @@ export const packageProject = () => ({ __typename: 'Project', }); +export const linksData = { + _links: { + webPath: '/gitlab-org/package-15', + }, +}; + export const packageVersions = () => [ { createdAt: '2021-08-10T09:33:54Z', id: 'gid://gitlab/Packages::Package/243', name: '@gitlab-org/package-15', status: 'DEFAULT', + canDestroy: true, tags: { nodes: packageTags() }, version: '1.0.1', + ...linksData, __typename: 'Package', }, { @@ -112,19 +120,14 @@ export const packageVersions = () => [ id: 'gid://gitlab/Packages::Package/244', name: '@gitlab-org/package-15', status: 'DEFAULT', + canDestroy: true, tags: { nodes: packageTags() }, version: '1.0.2', + ...linksData, __typename: 'Package', }, ]; -export const linksData = { - _links: { - webPath: '/gitlab-org/package-15', - __typeName: 'PackageLinks', - }, -}; - export const packageData = (extend) => ({ __typename: 'Package', id: 'gid://gitlab/Packages::Package/111', @@ -294,14 +297,6 @@ export const packageMetadataQuery = (packageType) => { }; }; -export const packageDestroyMutation = () => ({ - data: { - destroyPackage: { - errors: [], - }, - }, -}); - export const packagesDestroyMutation = () => ({ data: { destroyPackages: { @@ -329,25 +324,6 @@ export const packagesDestroyMutationError = () => ({ ], }); -export const packageDestroyMutationError = () => ({ - data: { - destroyPackage: null, - }, - errors: [ - { - message: - "The resource that you are attempting to access does not exist or you don't have permission to perform this action", - locations: [ - { - line: 2, - column: 3, - }, - ], - path: ['destroyPackage'], - }, - ], -}); - export const packageDestroyFilesMutation = () => ({ data: { destroyPackageFiles: { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index eb3b999c1ca..b494965a3cb 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -15,7 +15,7 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; 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 DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; +import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; import { FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, @@ -85,7 +85,7 @@ describe('PackagesApp', () => { provide, stubs: { PackageTitle, - DeletePackage, + DeletePackages, GlModal: { template: ` <div> @@ -128,7 +128,8 @@ describe('PackagesApp', () => { const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge'); const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message'); const findDependencyRows = () => wrapper.findAllComponents(DependencyRow); - const findDeletePackage = () => wrapper.findComponent(DeletePackage); + const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1); + const findDeletePackages = () => wrapper.findComponent(DeletePackages); afterEach(() => { wrapper.destroy(); @@ -267,7 +268,7 @@ describe('PackagesApp', () => { await waitForPromises(); - findDeletePackage().vm.$emit('end'); + findDeletePackageModal().vm.$emit('end'); expect(window.location.replace).toHaveBeenCalledWith( 'projectListUrl?showSuccessDeleteAlert=true', @@ -281,7 +282,7 @@ describe('PackagesApp', () => { await waitForPromises(); - findDeletePackage().vm.$emit('end'); + findDeletePackageModal().vm.$emit('end'); expect(window.location.replace).toHaveBeenCalledWith( 'groupListUrl?showSuccessDeleteAlert=true', @@ -595,13 +596,56 @@ describe('PackagesApp', () => { it('binds the correct props', async () => { const versionNodes = packageVersions(); - createComponent({ packageEntity: { versions: { nodes: versionNodes } } }); + createComponent(); + await waitForPromises(); expect(findVersionsList().props()).toMatchObject({ + canDestroy: true, versions: expect.arrayContaining(versionNodes), }); }); + + describe('delete packages', () => { + it('exists and has the correct props', async () => { + createComponent(); + + await waitForPromises(); + + expect(findDeletePackages().props()).toMatchObject({ + refetchQueries: [{ query: getPackageDetails, variables: {} }], + showSuccessAlert: true, + }); + }); + + it('deletePackages is bound to package-versions-list delete event', async () => { + createComponent(); + + await waitForPromises(); + + findVersionsList().vm.$emit('delete', [{ id: 1 }]); + + expect(findDeletePackages().emitted('start')).toEqual([[]]); + }); + + it('start and end event set loading correctly', async () => { + createComponent(); + + await waitForPromises(); + + findDeletePackages().vm.$emit('start'); + + await nextTick(); + + expect(findVersionsList().props('isLoading')).toBe(true); + + findDeletePackages().vm.$emit('end'); + + await nextTick(); + + expect(findVersionsList().props('isLoading')).toBe(false); + }); + }); }); describe('dependency links', () => { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js index b3cbd9f5dcf..a2ec527ce12 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -8,26 +8,18 @@ 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'; -import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; +import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue'; import { PROJECT_RESOURCE_TYPE, GROUP_RESOURCE_TYPE, GRAPHQL_PAGE_SIZE, EMPTY_LIST_HELP_URL, PACKAGE_HELP_URL, - DELETE_PACKAGES_ERROR_MESSAGE, - DELETE_PACKAGES_SUCCESS_MESSAGE, } from '~/packages_and_registries/package_registry/constants'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; -import { - packagesListQuery, - packageData, - pagination, - packagesDestroyMutation, - packagesDestroyMutationError, -} from '../mock_data'; +import { packagesListQuery, packageData, pagination } from '../mock_data'; jest.mock('~/flash'); @@ -53,12 +45,11 @@ describe('PackagesListApp', () => { filters: { packageName: 'foo', packageType: 'CONAN' }, }; - const findAlert = () => wrapper.findComponent(GlAlert); 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 findDeletePackages = () => wrapper.findComponent(DeletePackages); const mountComponent = ({ resolver = jest.fn().mockResolvedValue(packagesListQuery()), @@ -82,7 +73,7 @@ describe('PackagesListApp', () => { GlSprintf, GlLink, PackageList, - DeletePackage, + DeletePackages, }, }); }; @@ -243,26 +234,26 @@ describe('PackagesListApp', () => { }); }); - describe('delete package', () => { + describe('delete packages', () => { it('exists and has the correct props', async () => { mountComponent(); await waitForFirstRequest(); - expect(findDeletePackage().props()).toMatchObject({ + expect(findDeletePackages().props()).toMatchObject({ refetchQueries: [{ query: getPackagesQuery, variables: {} }], showSuccessAlert: true, }); }); - it('deletePackage is bound to package-list package:delete event', async () => { + it('deletePackages is bound to package-list delete event', async () => { mountComponent(); await waitForFirstRequest(); - findListComponent().vm.$emit('package:delete', { id: 1 }); + findListComponent().vm.$emit('delete', [{ id: 1 }]); - expect(findDeletePackage().emitted('start')).toEqual([[]]); + expect(findDeletePackages().emitted('start')).toEqual([[]]); }); it('start and end event set loading correctly', async () => { @@ -270,59 +261,17 @@ describe('PackagesListApp', () => { await waitForFirstRequest(); - findDeletePackage().vm.$emit('start'); + findDeletePackages().vm.$emit('start'); await nextTick(); expect(findListComponent().props('isLoading')).toBe(true); - findDeletePackage().vm.$emit('end'); + findDeletePackages().vm.$emit('end'); await nextTick(); expect(findListComponent().props('isLoading')).toBe(false); }); }); - - describe('bulk delete package', () => { - const items = [{ id: '1' }, { id: '2' }]; - - it('calls mutation with the right values and shows success alert', async () => { - const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation()); - mountComponent({ - mutationResolver, - }); - - await waitForFirstRequest(); - - findListComponent().vm.$emit('delete', items); - - expect(mutationResolver).toHaveBeenCalledWith({ - ids: items.map((item) => item.id), - }); - - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - expect(findAlert().props('variant')).toEqual('success'); - expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_SUCCESS_MESSAGE); - }); - - it('on error shows danger alert', async () => { - const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutationError()); - mountComponent({ - mutationResolver, - }); - - await waitForFirstRequest(); - - findListComponent().vm.$emit('delete', items); - - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - expect(findAlert().props('variant')).toEqual('danger'); - expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_ERROR_MESSAGE); - }); - }); }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js index daf0ee85fdf..0fbbf4ae58f 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js @@ -11,6 +11,7 @@ import { KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL, KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION, } from '~/packages_and_registries/settings/project/constants'; +import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql'; import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql'; import Tracking from '~/tracking'; import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data'; @@ -39,10 +40,13 @@ describe('Packages Cleanup Policy Settings Form', () => { label: 'packages_cleanup_policies', }; + const defaultQueryResolver = jest.fn().mockResolvedValue(packagesCleanupPolicyPayload()); + const findForm = () => wrapper.findComponent({ ref: 'form-element' }); const findSaveButton = () => wrapper.findByTestId('save-button'); const findKeepNDuplicatedPackageFilesDropdown = () => wrapper.findByTestId('keep-n-duplicated-package-files-dropdown'); + const findNextRunAt = () => wrapper.findByTestId('next-run-at'); const submitForm = async () => { findForm().trigger('submit'); @@ -77,10 +81,14 @@ describe('Packages Cleanup Policy Settings Form', () => { const mountComponentWithApollo = ({ provide = defaultProvidedValues, + queryResolver = defaultQueryResolver, mutationResolver, queryPayload = packagesCleanupPolicyPayload(), } = {}) => { - const requestHandlers = [[updatePackagesCleanupPolicyMutation, mutationResolver]]; + const requestHandlers = [ + [updatePackagesCleanupPolicyMutation, mutationResolver], + [packagesCleanupPolicyQuery, queryResolver], + ]; fakeApollo = createMockApollo(requestHandlers); @@ -160,6 +168,40 @@ describe('Packages Cleanup Policy Settings Form', () => { }); }); + describe('nextRunAt', () => { + it('when present renders time until next package cleanup', () => { + jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); + + mountComponent({ + props: { value: { ...defaultProps.value, nextRunAt: '2063-04-04T02:42:00Z' } }, + }); + + expect(findNextRunAt().text()).toMatchInterpolatedText( + 'Packages and assets will not be deleted until cleanup runs in about 2 hours.', + ); + }); + + it('renders message for cleanup when its before current date', () => { + jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); + + mountComponent({ + props: { value: { ...defaultProps.value, nextRunAt: '2063-03-04T00:42:00Z' } }, + }); + + expect(findNextRunAt().text()).toMatchInterpolatedText( + 'Packages and assets cleanup is ready to be executed when the next cleanup job runs.', + ); + }); + + it('when null hides time until next package cleanup', () => { + mountComponent({ + props: { value: { ...defaultProps.value, nextRunAt: null } }, + }); + + expect(findNextRunAt().exists()).toBe(false); + }); + }); + describe('form', () => { describe('actions', () => { describe('submit button', () => { @@ -209,7 +251,7 @@ describe('Packages Cleanup Policy Settings Form', () => { }); describe('form submit event', () => { - it('dispatches the correct apollo mutation', () => { + it('dispatches the correct apollo mutation and refetches query', async () => { const mutationResolver = jest .fn() .mockResolvedValue(packagesCleanupPolicyMutationPayload()); @@ -225,6 +267,12 @@ describe('Packages Cleanup Policy Settings Form', () => { projectPath: 'path', }, }); + + await waitForPromises(); + + expect(defaultQueryResolver).toHaveBeenCalledWith({ + projectPath: 'path', + }); }); it('tracks the submit event', () => { @@ -251,6 +299,18 @@ describe('Packages Cleanup Policy Settings Form', () => { expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE); }); + it('shows error toast when mutation responds with errors', async () => { + mountComponentWithApollo({ + mutationResolver: jest + .fn() + .mockResolvedValue(packagesCleanupPolicyMutationPayload({ errors: [new Error()] })), + }); + + await submitForm(); + + expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE); + }); + describe('when submit fails', () => { it('shows an error', async () => { mountComponentWithApollo({ 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 index aaca58d21bb..2e2d5e26d33 100644 --- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js @@ -54,6 +54,28 @@ describe('Registry List', () => { it('exists', () => { expect(findSelectAll().exists()).toBe(true); + expect(findSelectAll().attributes('aria-label')).toBe('Select all'); + expect(findSelectAll().attributes('disabled')).toBeUndefined(); + expect(findSelectAll().attributes('indeterminate')).toBeUndefined(); + }); + + it('sets disabled prop to true when items length is 0', () => { + mountComponent({ propsData: { ...defaultPropsData, items: [] } }); + + expect(findSelectAll().attributes('disabled')).toBe('true'); + }); + + it('when few are selected, sets indeterminate prop to true', async () => { + await findScopedSlotSelectButton(0).trigger('click'); + + expect(findSelectAll().attributes('indeterminate')).toBe('true'); + }); + + it('when all are selected, sets the right checkbox label', async () => { + findSelectAll().vm.$emit('change', true); + await nextTick(); + + expect(findSelectAll().attributes('aria-label')).toBe('Unselect all'); }); it('select and unselect all', async () => { @@ -63,7 +85,7 @@ describe('Registry List', () => { }); // simulate selection - findSelectAll().vm.$emit('input', true); + findSelectAll().vm.$emit('change', true); await nextTick(); // all rows selected @@ -72,12 +94,12 @@ describe('Registry List', () => { }); // simulate de-selection - findSelectAll().vm.$emit('input', ''); + findSelectAll().vm.$emit('change', false); await nextTick(); // no row is not selected items.forEach((item, index) => { - expect(findScopedSlotIsSelectedValue(index).text()).toBe(''); + expect(findScopedSlotIsSelectedValue(index).text()).toBe('false'); }); }); }); |