diff --git a/app/assets/javascripts/packages/list/components/package_title.vue b/app/assets/javascripts/packages/list/components/package_title.vue index 6176e15ffd4d9195305a7a6828defa4a807f2c7c..426ad150ea9a9a154a33b1d196f3182ecbd3c6b7 100644 --- a/app/assets/javascripts/packages/list/components/package_title.vue +++ b/app/assets/javascripts/packages/list/components/package_title.vue @@ -11,25 +11,25 @@ export default { MetadataItem, }, props: { - packagesCount: { + count: { type: Number, required: false, default: null, }, - packageHelpUrl: { + helpUrl: { type: String, required: true, }, }, computed: { showPackageCount() { - return Number.isInteger(this.packagesCount); + return Number.isInteger(this.count); }, packageAmountText() { - return n__(`%d Package`, `%d Packages`, this.packagesCount); + return n__(`%d Package`, `%d Packages`, this.count); }, infoMessages() { - return [{ text: LIST_INTRO_TEXT, link: this.packageHelpUrl }]; + return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }]; }, }, i18n: { diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue index b9d922bf1cfa5d9746d5ee8ac5254fe24d1fee6f..4c5fb0ee7c9e33602eb676c332d00a7d48839a10 100644 --- a/app/assets/javascripts/packages/list/components/packages_list_app.vue +++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue @@ -8,8 +8,6 @@ import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants'; -import PackageSearch from './package_search.vue'; -import PackageTitle from './package_title.vue'; import PackageList from './packages_list.vue'; export default { @@ -18,8 +16,38 @@ export default { GlLink, GlSprintf, PackageList, - PackageTitle, - PackageSearch, + PackageTitle: () => + import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'), + PackageSearch: () => + import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'), + InfrastructureTitle: () => + import( + /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue' + ), + InfrastructureSearch: () => + import( + /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue' + ), + }, + inject: { + titleComponent: { + from: 'titleComponent', + default: 'PackageTitle', + }, + searchComponent: { + from: 'searchComponent', + default: 'PackageSearch', + }, + emptyPageTitle: { + from: 'emptyPageTitle', + default: s__('PackageRegistry|There are no packages yet'), + }, + noResultsText: { + from: 'noResultsText', + default: s__( + 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', + ), + }, }, computed: { ...mapState({ @@ -38,7 +66,7 @@ export default { emptyStateTitle() { return this.emptySearch - ? s__('PackageRegistry|There are no packages yet') + ? this.emptyPageTitle : s__('PackageRegistry|Sorry, your filter produced no results'); }, }, @@ -77,24 +105,21 @@ export default { }, i18n: { widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), - noResults: s__( - 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', - ), }, }; </script> <template> <div> - <package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" /> - <package-search @update="requestPackagesList" /> + <component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" /> + <component :is="searchComponent" @update="requestPackagesList" /> <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> <template #empty-state> <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration"> <template #description> <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" /> - <gl-sprintf v-else :message="$options.i18n.noResults"> + <gl-sprintf v-else :message="noResultsText"> <template #noPackagesLink="{ content }"> <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> </template> diff --git a/app/assets/javascripts/packages/list/packages_list_app_bundle.js b/app/assets/javascripts/packages/list/packages_list_app_bundle.js index 58b09c1ebd151ba333a81a92c94f97f21fc328f2..2911cf70a33ca8648e720ddfe85a615c2a603323 100644 --- a/app/assets/javascripts/packages/list/packages_list_app_bundle.js +++ b/app/assets/javascripts/packages/list/packages_list_app_bundle.js @@ -1,11 +1,8 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; import Translate from '~/vue_shared/translate'; import PackagesListApp from './components/packages_list_app.vue'; import { createStore } from './stores'; -Vue.use(VueApollo); Vue.use(Translate); export default () => { @@ -13,14 +10,9 @@ export default () => { const store = createStore(); store.dispatch('setInitialState', el.dataset); - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); - return new Vue({ el, store, - apolloProvider, components: { PackagesListApp, }, diff --git a/app/assets/javascripts/packages/shared/components/package_icon_and_name.vue b/app/assets/javascripts/packages/shared/components/package_icon_and_name.vue new file mode 100644 index 0000000000000000000000000000000000000000..105f7bbe1328fbe5dacc3a72dba71ab2b6a6f999 --- /dev/null +++ b/app/assets/javascripts/packages/shared/components/package_icon_and_name.vue @@ -0,0 +1,17 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + name: 'PackageIconAndName', + components: { + GlIcon, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <gl-icon name="package" class="gl-ml-3 gl-mr-2" /> + <span><slot></slot></span> + </div> +</template> diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue index 172b356227a6b872fa26b02457001c9825f4f532..4de4c191e512b4d3712476f8dc3e1582e724f34c 100644 --- a/app/assets/javascripts/packages/shared/components/package_list_row.vue +++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; +import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { getPackageTypeLabel } from '../utils'; @@ -11,7 +11,6 @@ export default { name: 'PackageListRow', components: { GlButton, - GlIcon, GlLink, GlSprintf, GlTruncate, @@ -19,11 +18,23 @@ export default { PackagePath, PublishMethod, ListItem, + PackageIconAndName: () => + import(/* webpackChunkName: 'package_registry_components' */ './package_icon_and_name.vue'), + InfrastructureIconAndName: () => + import( + /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue' + ), }, directives: { GlTooltip: GlTooltipDirective, }, mixins: [timeagoMixin], + inject: { + iconComponent: { + from: 'iconComponent', + default: 'PackageIconAndName', + }, + }, props: { packageEntity: { type: Object, @@ -94,10 +105,9 @@ export default { </gl-sprintf> </div> - <div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type"> - <gl-icon name="package" class="gl-ml-3 gl-mr-2" /> - <span>{{ packageType }}</span> - </div> + <component :is="iconComponent" v-if="showPackageType"> + {{ packageType }} + </component> <package-path v-if="hasProjectLink" :path="packageEntity.project_path" /> </div> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue new file mode 100644 index 0000000000000000000000000000000000000000..3100a1a7296a1234441e3b345f13b0d1e95fd372 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue @@ -0,0 +1,17 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + name: 'InfrastructureIconAndName', + components: { + GlIcon, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <gl-icon name="infrastructure-registry" class="gl-ml-3 gl-mr-2" /> + <span>{{ s__('InfrastructureRegistry|Terraform') }}</span> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue new file mode 100644 index 0000000000000000000000000000000000000000..2ed17208404f3b02f915058eaa1436f4cf70d328 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue @@ -0,0 +1,45 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { LIST_KEY_PACKAGE_TYPE } from '~/packages/list/constants'; +import getTableHeaders from '~/packages/list/utils'; +import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; +import UrlSync from '~/vue_shared/components/url_sync.vue'; + +export default { + components: { RegistrySearch, UrlSync }, + computed: { + ...mapState({ + isGroupPage: (state) => state.config.isGroupPage, + sorting: (state) => state.sorting, + filter: (state) => state.filter, + }), + sortableFields() { + return getTableHeaders(this.isGroupPage).filter((h) => h.orderBy !== LIST_KEY_PACKAGE_TYPE); + }, + }, + methods: { + ...mapActions(['setSorting', 'setFilter']), + updateSorting(newValue) { + this.setSorting(newValue); + this.$emit('update'); + }, + }, +}; +</script> + +<template> + <url-sync> + <template #default="{ updateQuery }"> + <registry-search + :filter="filter" + :sorting="sorting" + :tokens="[]" + :sortable-fields="sortableFields" + @sorting:changed="updateSorting" + @filter:changed="setFilter" + @filter:submit="$emit('update')" + @query:changed="updateQuery" + /> + </template> + </url-sync> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue new file mode 100644 index 0000000000000000000000000000000000000000..2a479c65d0ca4064c010d443095d459895191774 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue @@ -0,0 +1,53 @@ +<script> +import { s__, n__ } from '~/locale'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; + +export default { + name: 'InfrastructureTitle', + components: { + TitleArea, + MetadataItem, + }, + props: { + count: { + type: Number, + required: false, + default: null, + }, + helpUrl: { + type: String, + required: true, + }, + }, + computed: { + showModuleCount() { + return Number.isInteger(this.count); + }, + moduleAmountText() { + return n__(`%d Module`, `%d Modules`, this.count); + }, + infoMessages() { + return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }]; + }, + }, + i18n: { + LIST_TITLE_TEXT: s__('InfrastructureRegistry|Infrastructure Registry'), + LIST_INTRO_TEXT: s__( + 'InfrastructureRegistry|Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}', + ), + }, +}; +</script> + +<template> + <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages"> + <template #metadata-amount> + <metadata-item + v-if="showModuleCount" + icon="infrastructure-registry" + :text="moduleAmountText" + /> + </template> + </title-area> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js new file mode 100644 index 0000000000000000000000000000000000000000..88ee8a4200e97e63bd23763ebfdd92e57c7ba5e2 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import { s__ } from '~/locale'; +import PackagesListApp from '~/packages/list/components/packages_list_app.vue'; +import { createStore } from '~/packages/list/stores'; +import Translate from '~/vue_shared/translate'; + +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-vue-packages-list'); + const store = createStore(); + store.dispatch('setInitialState', el.dataset); + + return new Vue({ + el, + store, + components: { + PackagesListApp, + }, + provide: { + titleComponent: 'InfrastructureTitle', + searchComponent: 'InfrastructureSearch', + iconComponent: 'InfrastructureIconAndName', + emptyPageTitle: s__('InfrastructureRegistry|You have no Terraform modules in your project'), + noResultsText: s__( + 'InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab.', + ), + }, + render(createElement) { + return createElement('packages-list-app'); + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js index c94782fdf1bf14cd7f8e133d102121e354d10246..dfb750eca41ea039e98a896055c8ac90f8c61b88 100644 --- a/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js +++ b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js @@ -1,3 +1,3 @@ -import initPackageList from '~/packages/list/packages_list_app_bundle'; +import initList from '~/packages_and_registries/infrastructure_registry/list_app_bundle'; -initPackageList(); +initList(); diff --git a/app/views/projects/packages/infrastructure_registry/index.html.haml b/app/views/projects/packages/infrastructure_registry/index.html.haml index e8be9051275c145751ca904bbbb626f92de5c8bf..5a118997ff97a6b2400885c08403cefbe6845fc6 100644 --- a/app/views/projects/packages/infrastructure_registry/index.html.haml +++ b/app/views/projects/packages/infrastructure_registry/index.html.haml @@ -5,6 +5,6 @@ .col-12 #js-vue-packages-list{ data: { resource_id: @project.id, page_type: 'project', - empty_list_help_url: help_page_path('user/packages/package_registry/index'), - empty_list_illustration: image_path('illustrations/no-packages.svg'), - package_help_url: help_page_path('user/packages/index') } } + empty_list_help_url: help_page_path('user/infrastructure/index'), + empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg'), + package_help_url: help_page_path('user/infrastructure/index') } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 50c249a90f43d3b32e81d2b1af87cd664dccc525..c5bb6cb8737fe9fe1101996905640f0bf6c4aa9e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -91,6 +91,11 @@ msgid_plural "%d Approvals" msgstr[0] "" msgstr[1] "" +msgid "%d Module" +msgid_plural "%d Modules" +msgstr[0] "" +msgstr[1] "" + msgid "%d Other" msgid_plural "%d Others" msgstr[0] "" @@ -16905,6 +16910,21 @@ msgstr "" msgid "Infrastructure Registry" msgstr "" +msgid "InfrastructureRegistry|Infrastructure Registry" +msgstr "" + +msgid "InfrastructureRegistry|Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}" +msgstr "" + +msgid "InfrastructureRegistry|Terraform" +msgstr "" + +msgid "InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab." +msgstr "" + +msgid "InfrastructureRegistry|You have no Terraform modules in your project" +msgstr "" + msgid "Inherited" msgstr "" diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap index 93f4981a6a368412a0516dd975ca533a3e3bb808..07aba62fef6cfb7f9d3e4432e84fb569d673134a 100644 --- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -2,11 +2,11 @@ exports[`packages_list_app renders 1`] = ` <div> - <package-title-stub - packagehelpurl="foo" + <div + help-url="foo" /> - <package-search-stub /> + <div /> <div> <section diff --git a/spec/frontend/packages/list/components/packages_list_app_spec.js b/spec/frontend/packages/list/components/packages_list_app_spec.js index 9ece92321366f5fa96543880e2b8e50f37e76ea4..4de2dd0789e927b7264b80cdbd7012adb623d7dc 100644 --- a/spec/frontend/packages/list/components/packages_list_app_spec.js +++ b/spec/frontend/packages/list/components/packages_list_app_spec.js @@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import createFlash from '~/flash'; import * as commonUtils from '~/lib/utils/common_utils'; -import PackageSearch from '~/packages/list/components/package_search.vue'; import PackageListApp from '~/packages/list/components/packages_list_app.vue'; import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; @@ -26,10 +25,19 @@ describe('packages_list_app', () => { }; const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' }; + // we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279 + const PackageSearch = { name: 'PackageSearch', template: '<div></div>' }; + const PackageTitle = { name: 'PackageTitle', template: '<div></div>' }; + const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' }; + const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' }; + const emptyListHelpUrl = 'helpUrl'; const findEmptyState = () => wrapper.find(GlEmptyState); const findListComponent = () => wrapper.find(PackageList); const findPackageSearch = () => wrapper.find(PackageSearch); + const findPackageTitle = () => wrapper.find(PackageTitle); + const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle); + const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch); const createStore = (filter = []) => { store = new Vuex.Store({ @@ -47,7 +55,7 @@ describe('packages_list_app', () => { store.dispatch = jest.fn(); }; - const mountComponent = () => { + const mountComponent = (provide) => { wrapper = shallowMount(PackageListApp, { localVue, store, @@ -57,7 +65,12 @@ describe('packages_list_app', () => { PackageList, GlSprintf, GlLink, + PackageSearch, + PackageTitle, + InfrastructureTitle, + InfrastructureSearch, }, + provide, }); }; @@ -194,6 +207,31 @@ describe('packages_list_app', () => { }); }); + describe('Infrastructure config', () => { + it('defaults to package registry components', () => { + mountComponent(); + + expect(findPackageSearch().exists()).toBe(true); + expect(findPackageTitle().exists()).toBe(true); + + expect(findInfrastructureTitle().exists()).toBe(false); + expect(findInfrastructureSearch().exists()).toBe(false); + }); + + it('mount different component based on the provided values', () => { + mountComponent({ + titleComponent: 'InfrastructureTitle', + searchComponent: 'InfrastructureSearch', + }); + + expect(findPackageSearch().exists()).toBe(false); + expect(findPackageTitle().exists()).toBe(false); + + expect(findInfrastructureTitle().exists()).toBe(true); + expect(findInfrastructureSearch().exists()).toBe(true); + }); + }); + describe('delete alert handling', () => { const { location } = window.location; const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`; diff --git a/spec/frontend/packages/list/components/packages_title_spec.js b/spec/frontend/packages/list/components/packages_title_spec.js index 3716e8daa7cdb527bd1d9eb457851c531703e9dc..a17f72e313378cd6cdad43eb9bd43544356ef415 100644 --- a/spec/frontend/packages/list/components/packages_title_spec.js +++ b/spec/frontend/packages/list/components/packages_title_spec.js @@ -11,7 +11,7 @@ describe('PackageTitle', () => { const findTitleArea = () => wrapper.find(TitleArea); const findMetadataItem = () => wrapper.find(MetadataItem); - const mountComponent = (propsData = { packageHelpUrl: 'foo' }) => { + const mountComponent = (propsData = { helpUrl: 'foo' }) => { wrapper = shallowMount(PackageTitle, { store, propsData, @@ -44,15 +44,15 @@ describe('PackageTitle', () => { }); describe.each` - packagesCount | exist | text - ${null} | ${false} | ${''} - ${undefined} | ${false} | ${''} - ${0} | ${true} | ${'0 Packages'} - ${1} | ${true} | ${'1 Package'} - ${2} | ${true} | ${'2 Packages'} - `('when packagesCount is $packagesCount metadata item', ({ packagesCount, exist, text }) => { + count | exist | text + ${null} | ${false} | ${''} + ${undefined} | ${false} | ${''} + ${0} | ${true} | ${'0 Packages'} + ${1} | ${true} | ${'1 Package'} + ${2} | ${true} | ${'2 Packages'} + `('when count is $count metadata item', ({ count, exist, text }) => { beforeEach(() => { - mountComponent({ packagesCount, packageHelpUrl: 'foo' }); + mountComponent({ count, helpUrl: 'foo' }); }); it(`is ${exist} that it exists`, () => { diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap index 77095f7c6113bbe51a88e3b96cea9c5dfc8f15c1..03b98478f3e82542b4c4f1f1cef2d0f8213a33ef 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap @@ -51,20 +51,7 @@ exports[`packages_list_row renders 1`] = ` <!----> - <div - class="d-flex align-items-center" - data-testid="package-type" - > - <gl-icon-stub - class="gl-ml-3 gl-mr-2" - name="package" - size="16" - /> - - <span> - Maven - </span> - </div> + <div /> <package-path-stub path="foo/bar/baz" diff --git a/spec/frontend/packages/shared/components/package_icon_and_name_spec.js b/spec/frontend/packages/shared/components/package_icon_and_name_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c96a570a29c562d5c6c1e9dd13d5befa1c15971f --- /dev/null +++ b/spec/frontend/packages/shared/components/package_icon_and_name_spec.js @@ -0,0 +1,32 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue'; + +describe('PackageIconAndName', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + + const mountComponent = () => { + wrapper = shallowMount(PackageIconAndName, { + slots: { + default: 'test', + }, + }); + }; + + it('has an icon', () => { + mountComponent(); + + const icon = findIcon(); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('package'); + }); + + it('renders the slot content', () => { + mountComponent(); + + expect(wrapper.text()).toBe('test'); + }); +}); diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js index 1c0ef7e353955bccfdfecdb1c90ce1c53aff8130..fd54cd0f25dc52365499833c8dbbea5f6d57c097 100644 --- a/spec/frontend/packages/shared/components/package_list_row_spec.js +++ b/spec/frontend/packages/shared/components/package_list_row_spec.js @@ -1,7 +1,9 @@ import { shallowMount } from '@vue/test-utils'; + import PackagesListRow from '~/packages/shared/components/package_list_row.vue'; import PackagePath from '~/packages/shared/components/package_path.vue'; import PackageTags from '~/packages/shared/components/package_tags.vue'; + import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { packageList } from '../../mock_data'; @@ -11,20 +13,30 @@ describe('packages_list_row', () => { const [packageWithoutTags, packageWithTags] = packageList; + const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' }; + const PackageIconAndName = { name: 'PackageIconAndName', template: '<div></div>' }; + const findPackageTags = () => wrapper.find(PackageTags); const findPackagePath = () => wrapper.find(PackagePath); const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]'); - const findPackageType = () => wrapper.find('[data-testid="package-type"]'); + const findPackageIconAndName = () => wrapper.find(PackageIconAndName); + const findInfrastructureIconAndName = () => wrapper.find(InfrastructureIconAndName); const mountComponent = ({ isGroup = false, packageEntity = packageWithoutTags, showPackageType = true, disableDelete = false, + provide, } = {}) => { wrapper = shallowMount(PackagesListRow, { store, - stubs: { ListItem }, + provide, + stubs: { + ListItem, + InfrastructureIconAndName, + PackageIconAndName, + }, propsData: { packageLink: 'foo', packageEntity, @@ -72,13 +84,13 @@ describe('packages_list_row', () => { it('shows the type when set', () => { mountComponent(); - expect(findPackageType().exists()).toBe(true); + expect(findPackageIconAndName().exists()).toBe(true); }); it('does not show the type when not set', () => { mountComponent({ showPackageType: false }); - expect(findPackageType().exists()).toBe(false); + expect(findPackageIconAndName().exists()).toBe(false); }); }); @@ -113,4 +125,25 @@ describe('packages_list_row', () => { expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]); }); }); + + describe('Infrastructure config', () => { + it('defaults to package registry components', () => { + mountComponent(); + + expect(findPackageIconAndName().exists()).toBe(true); + expect(findInfrastructureIconAndName().exists()).toBe(false); + }); + + it('mounts different component based on the provided values', () => { + mountComponent({ + provide: { + iconComponent: 'InfrastructureIconAndName', + }, + }); + + expect(findPackageIconAndName().exists()).toBe(false); + + expect(findInfrastructureIconAndName().exists()).toBe(true); + }); + }); }); 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/infrastructure_icon_and_name_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ef26c729691713590e93c4ad93a0633fbe667699 --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name_spec.js @@ -0,0 +1,28 @@ +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'; + +describe('InfrastructureIconAndName', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + + const mountComponent = () => { + wrapper = shallowMount(InfrastructureIconAndName, {}); + }; + + it('has an icon', () => { + mountComponent(); + + const icon = findIcon(); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('infrastructure-registry'); + }); + + it('has the type fixed to terraform', () => { + mountComponent(); + + expect(wrapper.text()).toBe('Terraform'); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_search_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..93d560fd08df7782e62291af770110ca59fc074f --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_search_spec.js @@ -0,0 +1,129 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'; +import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; +import UrlSync from '~/vue_shared/components/url_sync.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Infrastructure Search', () => { + let wrapper; + let store; + + const sortableFields = () => [ + { orderBy: 'name', label: 'Name' }, + { orderBy: 'project_path', label: 'Project' }, + { orderBy: 'version', label: 'Version' }, + { orderBy: 'created_at', label: 'Published' }, + ]; + + const findRegistrySearch = () => wrapper.findComponent(RegistrySearch); + const findUrlSync = () => wrapper.findComponent(UrlSync); + + const createStore = (isGroupPage) => { + const state = { + config: { + isGroupPage, + }, + sorting: { + orderBy: 'version', + sort: 'desc', + }, + filter: [], + }; + store = new Vuex.Store({ + state, + }); + store.dispatch = jest.fn(); + }; + + const mountComponent = (isGroupPage = false) => { + createStore(isGroupPage); + + wrapper = shallowMount(component, { + localVue, + store, + stubs: { + UrlSync, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('has a registry search component', () => { + mountComponent(); + + expect(findRegistrySearch().exists()).toBe(true); + expect(findRegistrySearch().props()).toMatchObject({ + filter: store.state.filter, + sorting: store.state.sorting, + tokens: [], + sortableFields: sortableFields(), + }); + }); + + it.each` + isGroupPage | page + ${false} | ${'project'} + ${true} | ${'group'} + `('in a $page page binds the right props', ({ isGroupPage }) => { + mountComponent(isGroupPage); + + expect(findRegistrySearch().props()).toMatchObject({ + filter: store.state.filter, + sorting: store.state.sorting, + tokens: [], + sortableFields: sortableFields(), + }); + }); + + it('on sorting:changed emits update event and calls vuex setSorting', () => { + const payload = { sort: 'foo' }; + + mountComponent(); + + findRegistrySearch().vm.$emit('sorting:changed', payload); + + expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload); + expect(wrapper.emitted('update')).toEqual([[]]); + }); + + it('on filter:changed calls vuex setFilter', () => { + const payload = ['foo']; + + mountComponent(); + + findRegistrySearch().vm.$emit('filter:changed', payload); + + expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload); + }); + + it('on filter:submit emits update event', () => { + mountComponent(); + + findRegistrySearch().vm.$emit('filter:submit'); + + expect(wrapper.emitted('update')).toEqual([[]]); + }); + + it('has a UrlSync component', () => { + mountComponent(); + + expect(findUrlSync().exists()).toBe(true); + }); + + it('on query:changed calls updateQuery from UrlSync', () => { + jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {}); + + mountComponent(); + + findRegistrySearch().vm.$emit('query:changed'); + + expect(UrlSync.methods.updateQuery).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_title_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..db6e175b0549a302ab25aca6b16101f41239acbb --- /dev/null +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/infrastructure_title_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; + +describe('Infrastructure Title', () => { + let wrapper; + let store; + + const findTitleArea = () => wrapper.find(TitleArea); + const findMetadataItem = () => wrapper.find(MetadataItem); + + const mountComponent = (propsData = { helpUrl: 'foo' }) => { + wrapper = shallowMount(component, { + store, + propsData, + stubs: { + TitleArea, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('title area', () => { + it('exists', () => { + mountComponent(); + + expect(findTitleArea().exists()).toBe(true); + }); + + it('has the correct props', () => { + mountComponent(); + + expect(findTitleArea().props()).toMatchObject({ + title: 'Infrastructure Registry', + infoMessages: [ + { + text: 'Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}', + link: 'foo', + }, + ], + }); + }); + }); + + describe.each` + count | exist | text + ${null} | ${false} | ${''} + ${undefined} | ${false} | ${''} + ${0} | ${true} | ${'0 Modules'} + ${1} | ${true} | ${'1 Module'} + ${2} | ${true} | ${'2 Modules'} + `('when count is $count metadata item', ({ count, exist, text }) => { + beforeEach(() => { + mountComponent({ count, helpUrl: 'foo' }); + }); + + it(`is ${exist} that it exists`, () => { + expect(findMetadataItem().exists()).toBe(exist); + }); + + if (exist) { + it('has the correct props', () => { + expect(findMetadataItem().props()).toMatchObject({ + icon: 'infrastructure-registry', + text, + }); + }); + } + }); +});