diff options
Diffstat (limited to 'spec/frontend/packages_and_registries/infrastructure_registry')
21 files changed, 1494 insertions, 43 deletions
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); + }); + }); +}); |