Commit 5755cea9 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '330847-convert-package-details-page-to-use-graphql' into 'master'

Add apollo and title component to package details refactor

See merge request gitlab-org/gitlab!65810
parents 31a77ba2 a48a03d9
...@@ -14,6 +14,8 @@ import { ...@@ -14,6 +14,8 @@ import {
GlTabs, GlTabs,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility'; import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
...@@ -24,12 +26,21 @@ import { s__, __ } from '~/locale'; ...@@ -24,12 +26,21 @@ import { s__, __ } from '~/locale';
// import PackageHistory from '~/packages/details/components/package_history.vue'; // import PackageHistory from '~/packages/details/components/package_history.vue';
// import PackageListRow from '~/packages/shared/components/package_list_row.vue'; // import PackageListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import { import {
PackageType, PACKAGE_TYPE_NUGET,
TrackingActions, PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
PULL_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGE_FILE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT, SHOW_DELETE_SUCCESS_ALERT,
} from '~/packages/shared/constants'; FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
import { packageTypeToTrackCategory } from '~/packages/shared/utils'; } from '~/packages_and_registries/package_registry/constants';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
export default { export default {
...@@ -42,7 +53,8 @@ export default { ...@@ -42,7 +53,8 @@ export default {
GlTab, GlTab,
GlTabs, GlTabs,
GlSprintf, GlSprintf,
PackageTitle: () => import('~/packages/details/components/package_title.vue'), PackageTitle: () =>
import('~/packages_and_registries/package_registry/components/details/package_title.vue'),
TerraformTitle: () => TerraformTitle: () =>
import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'), import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
PackagesListLoader, PackagesListLoader,
...@@ -59,6 +71,7 @@ export default { ...@@ -59,6 +71,7 @@ export default {
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: [ inject: [
'packageId',
'titleComponent', 'titleComponent',
'projectName', 'projectName',
'canDelete', 'canDelete',
...@@ -68,22 +81,53 @@ export default { ...@@ -68,22 +81,53 @@ export default {
'projectListUrl', 'projectListUrl',
'groupListUrl', 'groupListUrl',
], ],
trackingActions: { ...TrackingActions }, trackingActions: {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
PULL_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGE_FILE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
},
data() { data() {
return { return {
fileToDelete: null, fileToDelete: null,
packageEntity: {}, packageEntity: {},
}; };
}, },
apollo: {
packageEntity: {
query: getPackageDetails,
variables() {
return this.queryVariables;
},
update(data) {
return data.package;
},
error(error) {
createFlash({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
captureError: true,
error,
});
},
},
},
computed: { computed: {
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
};
},
packageFiles() { packageFiles() {
return this.packageEntity.packageFiles; return this.packageEntity.packageFiles;
}, },
isLoading() { isLoading() {
return false; return this.$apollo.queries.package;
}, },
isValidPackage() { isValidPackage() {
return Boolean(this.packageEntity.name); return Boolean(this.packageEntity?.name);
}, },
tracking() { tracking() {
return { return {
...@@ -97,10 +141,10 @@ export default { ...@@ -97,10 +141,10 @@ export default {
return this.packageEntity.dependency_links || []; return this.packageEntity.dependency_links || [];
}, },
showDependencies() { showDependencies() {
return this.packageEntity.package_type === PackageType.NUGET; return this.packageEntity.package_type === PACKAGE_TYPE_NUGET;
}, },
showFiles() { showFiles() {
return this.packageEntity?.package_type !== PackageType.COMPOSER; return this.packageEntity?.package_type !== PACKAGE_TYPE_COMPOSER;
}, },
}, },
methods: { methods: {
...@@ -113,7 +157,7 @@ export default { ...@@ -113,7 +157,7 @@ export default {
} }
}, },
async confirmPackageDeletion() { async confirmPackageDeletion() {
this.track(TrackingActions.DELETE_PACKAGE); this.track(DELETE_PACKAGE_TRACKING_ACTION);
await this.deletePackage(); await this.deletePackage();
...@@ -127,12 +171,12 @@ export default { ...@@ -127,12 +171,12 @@ export default {
window.location.replace(`${returnTo}?${modalQuery}`); window.location.replace(`${returnTo}?${modalQuery}`);
}, },
handleFileDelete(file) { handleFileDelete(file) {
this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE); this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
this.fileToDelete = { ...file }; this.fileToDelete = { ...file };
this.$refs.deleteFileModal.show(); this.$refs.deleteFileModal.show();
}, },
confirmFileDelete() { confirmFileDelete() {
this.track(TrackingActions.DELETE_PACKAGE_FILE); this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
// this.deletePackageFile(this.fileToDelete.id); // this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null; this.fileToDelete = null;
}, },
...@@ -176,7 +220,7 @@ export default { ...@@ -176,7 +220,7 @@ export default {
/> />
<div v-else class="packages-app"> <div v-else class="packages-app">
<component :is="titleComponent"> <component :is="titleComponent" :package-entity="packageEntity">
<template #delete-button> <template #delete-button>
<gl-button <gl-button
v-if="canDelete" v-if="canDelete"
......
<script>
import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
name: 'PackageTitle',
components: {
TitleArea,
GlIcon,
GlSprintf,
PackageTags,
MetadataItem,
GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
props: {
packageEntity: {
type: Object,
required: true,
},
},
data() {
return {
isDesktop: true,
};
},
computed: {
packageTypeDisplay() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
packagePipeline() {
return this.packageEntity.pipelines?.nodes[0];
},
packageIcon() {
if (this.packageEntity.packageType === PACKAGE_TYPE_NUGET) {
return this.packageEntity.metadata?.iconUrl || null;
}
return null;
},
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags?.nodes && this.packageEntity.tags?.nodes.length);
},
totalSize() {
return this.packageEntity.packageFiles
? numberToHumanSize(
this.packageEntity.packageFiles.nodes.reduce((acc, p) => acc + Number(p.size), 0),
)
: '0';
},
},
mounted() {
this.isDesktop = GlBreakpointInstance.isDesktop();
},
methods: {
dynamicSlotName(index) {
return `metadata-tag${index}`;
},
},
};
</script>
<template>
<title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</template>
<template v-if="packageTypeDisplay" #metadata-type>
<metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
</template>
<template #metadata-size>
<metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
</template>
<template v-if="packagePipeline" #metadata-pipeline>
<metadata-item
data-testid="pipeline-project"
icon="review-list"
:text="packagePipeline.project.name"
:link="packagePipeline.project.webUrl"
/>
</template>
<template v-if="packagePipeline && packagePipeline.ref" #metadata-ref>
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template>
<template v-if="isDesktop && hasTagsToDisplay" #metadata-tags>
<package-tags :tag-display-limit="2" :tags="packageEntity.tags.nodes" hide-label />
</template>
<!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap -->
<template
v-for="(tag, index) in packageEntity.tags.nodes"
v-else-if="hasTagsToDisplay"
#[dynamicSlotName(index)]
>
<gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm">
{{ tag.name }}
</gl-badge>
</template>
<template #right-actions>
<slot name="delete-button"></slot>
</template>
</title-area>
</template>
import { __, s__ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
export const PACKAGE_TYPE_NPM = 'NPM';
export const PACKAGE_TYPE_NUGET = 'NUGET';
export const PACKAGE_TYPE_PYPI = 'PYPI';
export const PACKAGE_TYPE_COMPOSER = 'COMPOSER';
export const PACKAGE_TYPE_RUBYGEMS = 'RUBYGEMS';
export const PACKAGE_TYPE_GENERIC = 'GENERIC';
export const PACKAGE_TYPE_DEBIAN = 'DEBIAN';
export const PACKAGE_TYPE_HELM = 'HELM';
export const PACKAGE_TYPE_TERRAFORM = 'terraform_module';
export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package';
export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package';
export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package';
export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file';
export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file';
export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file';
export const TrackingCategories = {
[PACKAGE_TYPE_MAVEN]: 'MavenPackages',
[PACKAGE_TYPE_NPM]: 'NpmPackages',
[PACKAGE_TYPE_CONAN]: 'ConanPackages',
};
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
__('PackageRegistry|Something went wrong while deleting the package file.'),
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
);
export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
export const PACKAGE_ERROR_STATUS = 'error';
export const PACKAGE_DEFAULT_STATUS = 'default';
export const PACKAGE_HIDDEN_STATUS = 'hidden';
export const PACKAGE_PROCESSING_STATUS = 'processing';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
});
query getPackageDetails($id: ID!) {
package(id: $id) {
id
name
packageType
version
createdAt
updatedAt
status
tags {
nodes {
id
name
}
}
pipelines(first: 3) {
nodes {
project {
name
webUrl
}
}
}
packageFiles(first: 1000) {
nodes {
id
fileMd5
fileName
fileSha1
fileSha256
size
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import PackagesApp from '../components/details/app.vue';
Vue.use(Translate); Vue.use(Translate);
...@@ -14,6 +15,7 @@ export default () => { ...@@ -14,6 +15,7 @@ export default () => {
const { canDelete, ...datasetOptions } = el.dataset; const { canDelete, ...datasetOptions } = el.dataset;
return new Vue({ return new Vue({
el, el,
apolloProvider,
provide: { provide: {
canDelete: parseBoolean(canDelete), canDelete: parseBoolean(canDelete),
titleComponent: 'PackageTitle', titleComponent: 'PackageTitle',
......
import { s__ } from '~/locale';
import {
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_PYPI,
PACKAGE_TYPE_COMPOSER,
PACKAGE_TYPE_RUBYGEMS,
PACKAGE_TYPE_GENERIC,
PACKAGE_TYPE_DEBIAN,
PACKAGE_TYPE_HELM,
} from './constants';
export const getPackageTypeLabel = (packageType) => {
switch (packageType) {
case PACKAGE_TYPE_CONAN:
return s__('PackageRegistry|Conan');
case PACKAGE_TYPE_MAVEN:
return s__('PackageRegistry|Maven');
case PACKAGE_TYPE_NPM:
return s__('PackageRegistry|npm');
case PACKAGE_TYPE_NUGET:
return s__('PackageRegistry|NuGet');
case PACKAGE_TYPE_PYPI:
return s__('PackageRegistry|PyPI');
case PACKAGE_TYPE_RUBYGEMS:
return s__('PackageRegistry|RubyGems');
case PACKAGE_TYPE_COMPOSER:
return s__('PackageRegistry|Composer');
case PACKAGE_TYPE_GENERIC:
return s__('PackageRegistry|Generic');
case PACKAGE_TYPE_DEBIAN:
return s__('PackageRegistry|Debian');
case PACKAGE_TYPE_HELM:
return s__('PackageRegistry|Helm');
default:
return null;
}
};
...@@ -64,9 +64,10 @@ module PackagesHelper ...@@ -64,9 +64,10 @@ module PackagesHelper
project.container_repositories.exists? project.container_repositories.exists?
end end
def package_details_data(project, package = nil) def package_details_data(project, package, use_presenter = false)
{ {
package: package ? package_from_presenter(package) : nil, package: use_presenter ? package_from_presenter(package) : nil,
package_id: package.id,
can_delete: can?(current_user, :destroy_package, project).to_s, can_delete: can?(current_user, :destroy_package, project).to_s,
svg_path: image_path('illustrations/no-packages.svg'), svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm), npm_path: package_registry_instance_url(:npm),
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
.row .row
.col-12 .col-12
- if Feature.enabled?(:package_details_apollo) - if Feature.enabled?(:package_details_apollo)
#js-vue-packages-detail-new{ data: package_details_data(@project) } #js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
- else - else
#js-vue-packages-detail{ data: package_details_data(@project, @package) } #js-vue-packages-detail{ data: package_details_data(@project, @package, true) }
...@@ -23354,6 +23354,9 @@ msgstr "" ...@@ -23354,6 +23354,9 @@ msgstr ""
msgid "PackageRegistry|Delete package" msgid "PackageRegistry|Delete package"
msgstr "" msgstr ""
msgid "PackageRegistry|Failed to load the package data"
msgstr ""
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}" msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-flex-direction-column gl-flex-grow-1"
>
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
@gitlab-org/package-15
</h1>
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="npm"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="800.00 KiB"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object]"
/>
</div>
</div>
</div>
<!---->
</div>
<p />
</div>
`;
exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-flex-direction-column gl-flex-grow-1"
>
<div
class="gl-display-flex"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
@gitlab-org/package-15
</h1>
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="npm"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="800.00 KiB"
texttooltip=""
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object]"
/>
</div>
</div>
</div>
<!---->
</div>
<p />
</div>
`;
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue'; import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import { FETCH_PACKAGE_DETAILS_ERROR_MESSAGE } from '~/packages_and_registries/package_registry/constants';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import { packageDetailsQuery, packageData } from '../../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
describe('PackagesApp', () => { describe('PackagesApp', () => {
let wrapper; let wrapper;
let apolloProvider;
function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()) } = {}) {
localVue.use(VueApollo);
const requestHandlers = [[getPackageDetails, resolver]];
apolloProvider = createMockApollo(requestHandlers);
function createComponent() {
wrapper = shallowMount(PackagesApp, { wrapper = shallowMount(PackagesApp, {
localVue,
apolloProvider,
provide: { provide: {
titleComponent: 'titleComponent', packageId: '111',
titleComponent: 'PackageTitle',
projectName: 'projectName', projectName: 'projectName',
canDelete: 'canDelete', canDelete: 'canDelete',
svgPath: 'svgPath', svgPath: 'svgPath',
...@@ -21,7 +42,8 @@ describe('PackagesApp', () => { ...@@ -21,7 +42,8 @@ describe('PackagesApp', () => {
}); });
} }
const emptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -30,6 +52,29 @@ describe('PackagesApp', () => { ...@@ -30,6 +52,29 @@ describe('PackagesApp', () => {
it('renders an empty state component', () => { it('renders an empty state component', () => {
createComponent(); createComponent();
expect(emptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
});
it('renders the app and displays the package title', async () => {
createComponent();
await waitForPromises();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props()).toMatchObject({
packageEntity: expect.objectContaining(packageData()),
});
});
it('emits an error message if the load fails', async () => {
createComponent({ resolver: jest.fn().mockRejectedValue() });
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
}),
);
}); });
}); });
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import {
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { packageData, packageFiles, packageTags, packagePipelines } from '../../mock_data';
const packageWithTags = {
...packageData(),
tags: { nodes: packageTags() },
packageFiles: { nodes: packageFiles() },
};
describe('PackageTitle', () => {
let wrapper;
function createComponent(packageEntity = packageWithTags) {
wrapper = shallowMountExtended(PackageTitle, {
propsData: { packageEntity },
stubs: {
TitleArea,
},
});
return wrapper.vm.$nextTick();
}
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findPackageType = () => wrapper.findByTestId('package-type');
const findPackageSize = () => wrapper.findByTestId('package-size');
const findPipelineProject = () => wrapper.findByTestId('pipeline-project');
const findPackageRef = () => wrapper.findByTestId('package-ref');
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackageBadges = () => wrapper.findAllByTestId('tag-badge');
afterEach(() => {
wrapper.destroy();
});
describe('renders', () => {
it('without tags', async () => {
await createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('with tags', async () => {
await createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('with tags on mobile', async () => {
jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
await createComponent();
await wrapper.vm.$nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
});
});
describe('package title', () => {
it('is correctly bound', async () => {
await createComponent();
expect(findTitleArea().props('title')).toBe(packageData().name);
});
});
describe('package icon', () => {
const iconUrl = 'a-fake-src';
it('shows an icon when present and package type is NUGET', async () => {
await createComponent({
...packageData(),
packageType: PACKAGE_TYPE_NUGET,
metadata: { iconUrl },
});
expect(findTitleArea().props('avatar')).toBe(iconUrl);
});
it('hides the icon when not present', async () => {
await createComponent();
expect(findTitleArea().props('avatar')).toBe(null);
});
});
describe.each`
packageType | text
${PACKAGE_TYPE_CONAN} | ${'Conan'}
${PACKAGE_TYPE_MAVEN} | ${'Maven'}
${PACKAGE_TYPE_NPM} | ${'npm'}
${PACKAGE_TYPE_NUGET} | ${'NuGet'}
`(`package type`, ({ packageType, text }) => {
beforeEach(() => createComponent({ ...packageData, packageType }));
it(`${packageType} should render ${text}`, () => {
expect(findPackageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' }));
});
});
describe('calculates the package size', () => {
it('correctly calculates when there is only 1 file', async () => {
await createComponent({ ...packageData(), packageFiles: { nodes: [packageFiles()[0]] } });
expect(findPackageSize().props()).toMatchObject({ text: '400.00 KiB', icon: 'disk' });
});
it('correctly calculates when there are multiple files', async () => {
await createComponent();
expect(findPackageSize().props('text')).toBe('800.00 KiB');
});
});
describe('package tags', () => {
it('displays the package-tags component when the package has tags', async () => {
await createComponent();
expect(findPackageTags().exists()).toBe(true);
});
it('does not display the package-tags component when there are no tags', async () => {
await createComponent({ ...packageData(), tags: { nodes: [] } });
expect(findPackageTags().exists()).toBe(false);
});
});
describe('package ref', () => {
it('does not display the ref if missing', async () => {
await createComponent();
expect(findPackageRef().exists()).toBe(false);
});
it('correctly shows the package ref if there is one', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines({ ref: 'test' }) },
});
expect(findPackageRef().props()).toMatchObject({
text: 'test',
icon: 'branch',
});
});
});
describe('pipeline project', () => {
it('does not display the project if missing', async () => {
await createComponent();
expect(findPipelineProject().exists()).toBe(false);
});
it('correctly shows the pipeline project if there is one', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines() },
});
expect(findPipelineProject().props()).toMatchObject({
text: packagePipelines()[0].project.name,
icon: 'review-list',
link: packagePipelines()[0].project.webUrl,
});
});
});
});
export const packageTags = () => [
{ id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/85', name: 'bananas_7', __typename: 'PackageTag' },
];
export const packagePipelines = (extend) => [
{
project: {
name: 'project14',
webUrl: 'http://gdk.test:3000/namespace14/project14',
__typename: 'Project',
},
...extend,
__typename: 'Pipeline',
},
];
export const packageFiles = () => [
{
id: 'gid://gitlab/Packages::PackageFile/118',
fileMd5: null,
fileName: 'foo-1.0.1.tgz',
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad',
fileSha256: null,
size: '409600',
__typename: 'PackageFile',
},
{
id: 'gid://gitlab/Packages::PackageFile/119',
fileMd5: null,
fileName: 'foo-1.0.2.tgz',
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss',
fileSha256: null,
size: '409600',
__typename: 'PackageFile',
},
];
export const packageData = (extend) => ({
id: 'gid://gitlab/Packages::Package/111',
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
createdAt: '2020-08-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
status: 'DEFAULT',
...extend,
});
export const packageDetailsQuery = () => ({
data: {
package: {
...packageData(),
tags: {
nodes: packageTags(),
__typename: 'PackageTagConnection',
},
pipelines: {
nodes: packagePipelines(),
__typename: 'PipelineConnection',
},
packageFiles: {
nodes: packageFiles(),
__typename: 'PackageFileConnection',
},
__typename: 'PackageDetailsType',
},
},
});
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
describe('Packages shared utils', () => {
describe('getPackageTypeLabel', () => {
describe.each`
packageType | expectedResult
${'CONAN'} | ${'Conan'}
${'MAVEN'} | ${'Maven'}
${'NPM'} | ${'npm'}
${'NUGET'} | ${'NuGet'}
${'PYPI'} | ${'PyPI'}
${'RUBYGEMS'} | ${'RubyGems'}
${'COMPOSER'} | ${'Composer'}
${'DEBIAN'} | ${'Debian'}
${'HELM'} | ${'Helm'}
${'FOO'} | ${null}
`(`package type`, ({ packageType, expectedResult }) => {
it(`${packageType} should show as ${expectedResult}`, () => {
expect(getPackageTypeLabel(packageType)).toBe(expectedResult);
});
});
});
});
...@@ -219,4 +219,25 @@ RSpec.describe PackagesHelper do ...@@ -219,4 +219,25 @@ RSpec.describe PackagesHelper do
it { is_expected.to eq(expected_result) } it { is_expected.to eq(expected_result) }
end end
end end
describe '#package_details_data' do
let_it_be(:package) { create(:package) }
before do
allow(helper).to receive(:current_user) { project.owner }
allow(helper).to receive(:can?) { true }
end
it 'when use_presenter is true populate the package key' do
result = helper.package_details_data(project, package, true)
expect(result[:package]).not_to be_nil
end
it 'when use_presenter is false the package key is nil' do
result = helper.package_details_data(project, package, false)
expect(result[:package]).to be_nil
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment