Commit 25d58154 authored by Natalia Tepluhina's avatar Natalia Tepluhina

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

Refactor and enable package files in package details

See merge request gitlab-org/gitlab!67674
parents 986fc63d 7df4f855
...@@ -20,13 +20,14 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; ...@@ -20,13 +20,14 @@ 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';
// import DependencyRow from '~/packages/details/components/dependency_row.vue'; // import DependencyRow from '~/packages/details/components/dependency_row.vue';
// import PackageFiles from '~/packages/details/components/package_files.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 { packageTypeToTrackCategory } from '~/packages/shared/utils';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import { import {
PACKAGE_TYPE_NUGET, PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_COMPOSER, PACKAGE_TYPE_COMPOSER,
...@@ -40,8 +41,12 @@ import { ...@@ -40,8 +41,12 @@ import {
SHOW_DELETE_SUCCESS_ALERT, SHOW_DELETE_SUCCESS_ALERT,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants'; } from '~/packages_and_registries/package_registry/constants';
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
...@@ -55,17 +60,14 @@ export default { ...@@ -55,17 +60,14 @@ export default {
GlTab, GlTab,
GlTabs, GlTabs,
GlSprintf, GlSprintf,
PackageTitle: () => PackageTitle,
import('~/packages_and_registries/package_registry/components/details/package_title.vue'),
TerraformTitle: () =>
import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
PackagesListLoader, PackagesListLoader,
// PackageListRow, // PackageListRow,
// DependencyRow, // DependencyRow,
PackageHistory, PackageHistory,
AdditionalMetadata, AdditionalMetadata,
InstallationCommands, InstallationCommands,
// PackageFiles, PackageFiles,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -74,7 +76,6 @@ export default { ...@@ -74,7 +76,6 @@ export default {
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: [ inject: [
'packageId', 'packageId',
'titleComponent',
'projectName', 'projectName',
'canDelete', 'canDelete',
'svgPath', 'svgPath',
...@@ -123,7 +124,7 @@ export default { ...@@ -123,7 +124,7 @@ export default {
}; };
}, },
packageFiles() { packageFiles() {
return this.packageEntity.packageFiles; return this.packageEntity?.packageFiles?.nodes;
}, },
isLoading() { isLoading() {
return this.$apollo.queries.packageEntity.loading; return this.$apollo.queries.packageEntity.loading;
...@@ -133,7 +134,7 @@ export default { ...@@ -133,7 +134,7 @@ export default {
}, },
tracking() { tracking() {
return { return {
category: packageTypeToTrackCategory(this.packageEntity.package_type), category: packageTypeToTrackCategory(this.packageEntity.packageType),
}; };
}, },
hasVersions() { hasVersions() {
...@@ -143,10 +144,10 @@ export default { ...@@ -143,10 +144,10 @@ export default {
return this.packageEntity.dependency_links || []; return this.packageEntity.dependency_links || [];
}, },
showDependencies() { showDependencies() {
return this.packageEntity.package_type === PACKAGE_TYPE_NUGET; return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
}, },
showFiles() { showFiles() {
return this.packageEntity?.package_type !== PACKAGE_TYPE_COMPOSER; return this.packageEntity?.packageType !== PACKAGE_TYPE_COMPOSER;
}, },
}, },
methods: { methods: {
...@@ -158,19 +159,17 @@ export default { ...@@ -158,19 +159,17 @@ export default {
// this.fetchPackageVersions(); // this.fetchPackageVersions();
} }
}, },
deletePackage() { async deletePackage() {
return this.$apollo const { data } = await this.$apollo.mutate({
.mutate({ mutation: destroyPackageMutation,
mutation: destroyPackageMutation, variables: {
variables: { id: this.packageEntity.id,
id: this.packageEntity.id, },
}, });
})
.then(({ data }) => { if (data?.destroyPackage?.errors[0]) {
if (data?.destroyPackage?.errors[0]) { throw data.destroyPackage.errors[0];
throw data.destroyPackage.errors[0]; }
}
});
}, },
async confirmPackageDeletion() { async confirmPackageDeletion() {
this.track(DELETE_PACKAGE_TRACKING_ACTION); this.track(DELETE_PACKAGE_TRACKING_ACTION);
...@@ -195,6 +194,37 @@ export default { ...@@ -195,6 +194,37 @@ export default {
}); });
} }
}, },
async deletePackageFile(id) {
try {
const { data } = await this.$apollo.mutate({
mutation: destroyPackageFileMutation,
variables: {
id,
},
awaitRefetchQueries: true,
refetchQueries: [
{
query: getPackageDetails,
variables: this.queryVariables,
},
],
});
if (data?.destroyPackageFile?.errors[0]) {
throw data.destroyPackageFile.errors[0];
}
createFlash({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
type: 'success',
});
} catch (error) {
createFlash({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
type: 'warning',
captureError: true,
error,
});
}
},
handleFileDelete(file) { handleFileDelete(file) {
this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION); this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
this.fileToDelete = { ...file }; this.fileToDelete = { ...file };
...@@ -202,7 +232,7 @@ export default { ...@@ -202,7 +232,7 @@ export default {
}, },
confirmFileDelete() { confirmFileDelete() {
this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
// this.deletePackageFile(this.fileToDelete.id); this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null; this.fileToDelete = null;
}, },
}, },
...@@ -245,7 +275,7 @@ export default { ...@@ -245,7 +275,7 @@ export default {
/> />
<div v-else class="packages-app"> <div v-else class="packages-app">
<component :is="titleComponent" :package-entity="packageEntity"> <package-title :package-entity="packageEntity">
<template #delete-button> <template #delete-button>
<gl-button <gl-button
v-if="canDelete" v-if="canDelete"
...@@ -258,7 +288,7 @@ export default { ...@@ -258,7 +288,7 @@ export default {
{{ __('Delete') }} {{ __('Delete') }}
</gl-button> </gl-button>
</template> </template>
</component> </package-title>
<gl-tabs> <gl-tabs>
<gl-tab :title="__('Detail')"> <gl-tab :title="__('Detail')">
...@@ -270,13 +300,12 @@ export default { ...@@ -270,13 +300,12 @@ export default {
<additional-metadata :package-entity="packageEntity" /> <additional-metadata :package-entity="packageEntity" />
</div> </div>
<!-- <package-files <package-files
v-if="showFiles" v-if="showFiles"
:package-files="packageFiles" :package-files="packageFiles"
:can-delete="canDelete"
@download-file="track($options.trackingActions.PULL_PACKAGE)" @download-file="track($options.trackingActions.PULL_PACKAGE)"
@delete-file="handleFileDelete" @delete-file="handleFileDelete"
/> --> />
</gl-tab> </gl-tab>
<gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab"> <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
...@@ -288,11 +317,11 @@ export default { ...@@ -288,11 +317,11 @@ export default {
</template> </template>
<template v-if="packageDependencies.length > 0"> <template v-if="packageDependencies.length > 0">
<dependency-row <!-- <dependency-row
v-for="(dep, index) in packageDependencies" v-for="(dep, index) in packageDependencies"
:key="index" :key="index"
:dependency="dep" :dependency="dep"
/> /> -->
</template> </template>
<p v-else class="gl-mt-3" data-testid="no-dependencies-message"> <p v-else class="gl-mt-3" data-testid="no-dependencies-message">
...@@ -329,6 +358,7 @@ export default { ...@@ -329,6 +358,7 @@ export default {
<gl-modal <gl-modal
ref="deleteModal" ref="deleteModal"
modal-id="delete-modal" modal-id="delete-modal"
data-testid="delete-modal"
:action-primary="$options.modal.packageDeletePrimaryAction" :action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction" :action-cancel="$options.modal.cancelAction"
@primary="confirmPackageDeletion" @primary="confirmPackageDeletion"
...@@ -351,6 +381,7 @@ export default { ...@@ -351,6 +381,7 @@ export default {
modal-id="delete-file-modal" modal-id="delete-file-modal"
:action-primary="$options.modal.fileDeletePrimaryAction" :action-primary="$options.modal.fileDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction" :action-cancel="$options.modal.cancelAction"
data-testid="delete-file-modal"
@primary="confirmFileDelete" @primary="confirmFileDelete"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
> >
......
...@@ -22,17 +22,13 @@ export default { ...@@ -22,17 +22,13 @@ export default {
FileSha, FileSha,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: ['canDelete'],
props: { props: {
packageFiles: { packageFiles: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
}, },
canDelete: {
type: Boolean,
default: false,
required: false,
},
}, },
computed: { computed: {
filesTableRows() { filesTableRows() {
...@@ -43,6 +39,8 @@ export default { ...@@ -43,6 +39,8 @@ export default {
})); }));
}, },
showCommitColumn() { showCommitColumn() {
// note that this is always false for now since we do not return
// pipelines associated to files for performance concerns
return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); return this.filesTableRows.some((row) => Boolean(row.pipeline?.id));
}, },
filesTableHeaderFields() { filesTableHeaderFields() {
...@@ -80,7 +78,7 @@ export default { ...@@ -80,7 +78,7 @@ export default {
return numberToHumanSize(size); return numberToHumanSize(size);
}, },
hasDetails(item) { hasDetails(item) {
return item.file_sha256 || item.file_md5 || item.file_sha1; return item.fileSha256 || item.fileMd5 || item.fileSha1;
}, },
}, },
i18n: { i18n: {
...@@ -107,32 +105,32 @@ export default { ...@@ -107,32 +105,32 @@ export default {
@click="toggleDetails" @click="toggleDetails"
/> />
<gl-link <gl-link
:href="item.download_path" :href="item.downloadPath"
class="gl-text-gray-500" class="gl-text-gray-500"
data-testid="download-link" data-testid="download-link"
@click="$emit('download-file')" @click="$emit('download-file')"
> >
<file-icon <file-icon
:file-name="item.file_name" :file-name="item.fileName"
css-classes="gl-relative file-icon" css-classes="gl-relative file-icon"
class="gl-mr-1 gl-relative" class="gl-mr-1 gl-relative"
/> />
<span>{{ item.file_name }}</span> <span>{{ item.fileName }}</span>
</gl-link> </gl-link>
</template> </template>
<template #cell(commit)="{ item }"> <template #cell(commit)="{ item }">
<gl-link <gl-link
v-if="item.pipeline && item.pipeline.project" v-if="item.pipeline && item.pipeline"
:href="item.pipeline.project.commit_url" :href="item.pipeline.commitPath"
class="gl-text-gray-500" class="gl-text-gray-500"
data-testid="commit-link" data-testid="commit-link"
>{{ item.pipeline.git_commit_message }}</gl-link >{{ item.pipeline.sha }}</gl-link
> >
</template> </template>
<template #cell(created)="{ item }"> <template #cell(created)="{ item }">
<time-ago-tooltip :time="item.created_at" /> <time-ago-tooltip :time="item.createdAt" />
</template> </template>
<template #cell(actions)="{ item }"> <template #cell(actions)="{ item }">
...@@ -151,13 +149,13 @@ export default { ...@@ -151,13 +149,13 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100" class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100"
> >
<file-sha <file-sha
v-if="item.file_sha256" v-if="item.fileSha256"
data-testid="sha-256" data-testid="sha-256"
title="SHA-256" title="SHA-256"
:sha="item.file_sha256" :sha="item.fileSha256"
/> />
<file-sha v-if="item.file_md5" data-testid="md5" title="MD5" :sha="item.file_md5" /> <file-sha v-if="item.fileMd5" data-testid="md5" title="MD5" :sha="item.fileMd5" />
<file-sha v-if="item.file_sha1" data-testid="sha-1" title="SHA-1" :sha="item.file_sha1" /> <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" />
</div> </div>
</template> </template>
</gl-table> </gl-table>
......
mutation destroyPackageFile($id: PackagesPackageFileID!) {
destroyPackageFile(input: { id: $id }) {
errors
}
}
...@@ -38,6 +38,8 @@ query getPackageDetails($id: ID!) { ...@@ -38,6 +38,8 @@ query getPackageDetails($id: ID!) {
fileSha1 fileSha1
fileSha256 fileSha256
size size
createdAt
downloadPath
} }
} }
metadata { metadata {
......
...@@ -18,7 +18,6 @@ export default () => { ...@@ -18,7 +18,6 @@ export default () => {
apolloProvider, apolloProvider,
provide: { provide: {
canDelete: parseBoolean(canDelete), canDelete: parseBoolean(canDelete),
titleComponent: 'PackageTitle',
...datasetOptions, ...datasetOptions,
}, },
render(createElement) { render(createElement) {
......
import { GlEmptyState, GlModal } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
...@@ -10,13 +11,19 @@ import createFlash from '~/flash'; ...@@ -10,13 +11,19 @@ import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
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 InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import { import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
} from '~/packages_and_registries/package_registry/constants'; } from '~/packages_and_registries/package_registry/constants';
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import { import {
packageDetailsQuery, packageDetailsQuery,
...@@ -24,6 +31,9 @@ import { ...@@ -24,6 +31,9 @@ import {
emptyPackageDetailsQuery, emptyPackageDetailsQuery,
packageDestroyMutation, packageDestroyMutation,
packageDestroyMutationError, packageDestroyMutationError,
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
} from '../../mock_data'; } from '../../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -50,12 +60,14 @@ describe('PackagesApp', () => { ...@@ -50,12 +60,14 @@ describe('PackagesApp', () => {
function createComponent({ function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()), mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
} = {}) { } = {}) {
localVue.use(VueApollo); localVue.use(VueApollo);
const requestHandlers = [ const requestHandlers = [
[getPackageDetails, resolver], [getPackageDetails, resolver],
[destroyPackageMutation, mutationResolver], [destroyPackageMutation, mutationResolver],
[destroyPackageFileMutation, fileDeleteMutationResolver],
]; ];
apolloProvider = createMockApollo(requestHandlers); apolloProvider = createMockApollo(requestHandlers);
...@@ -63,7 +75,15 @@ describe('PackagesApp', () => { ...@@ -63,7 +75,15 @@ describe('PackagesApp', () => {
localVue, localVue,
apolloProvider, apolloProvider,
provide, provide,
stubs: { PackageTitle }, stubs: {
PackageTitle,
GlModal: {
template: '<div></div>',
methods: {
show: jest.fn(),
},
},
},
}); });
} }
...@@ -72,8 +92,10 @@ describe('PackagesApp', () => { ...@@ -72,8 +92,10 @@ describe('PackagesApp', () => {
const findPackageHistory = () => wrapper.findComponent(PackageHistory); const findPackageHistory = () => wrapper.findComponent(PackageHistory);
const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata); const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata);
const findInstallationCommands = () => wrapper.findComponent(InstallationCommands); const findInstallationCommands = () => wrapper.findComponent(InstallationCommands);
const findDeleteModal = () => wrapper.findComponent(GlModal); const findDeleteModal = () => wrapper.findByTestId('delete-modal');
const findDeleteButton = () => wrapper.findByTestId('delete-package'); const findDeleteButton = () => wrapper.findByTestId('delete-package');
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -240,4 +262,104 @@ describe('PackagesApp', () => { ...@@ -240,4 +262,104 @@ describe('PackagesApp', () => {
}); });
}); });
}); });
describe('package files', () => {
it('renders the package files component and has the right props', async () => {
const expectedFile = { ...packageFiles()[0] };
// eslint-disable-next-line no-underscore-dangle
delete expectedFile.__typename;
createComponent();
await waitForPromises();
expect(findPackageFiles().exists()).toBe(true);
expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
});
it('does not render the package files table when the package is composer', async () => {
createComponent({
resolver: jest
.fn()
.mockResolvedValue(packageDetailsQuery({ packageType: PACKAGE_TYPE_COMPOSER })),
});
await waitForPromises();
expect(findPackageFiles().exists()).toBe(false);
});
describe('deleting a file', () => {
const [fileToDelete] = packageFiles();
const doDeleteFile = () => {
findPackageFiles().vm.$emit('delete-file', fileToDelete);
findDeleteFileModal().vm.$emit('primary');
return waitForPromises();
};
it('opens a confirmation modal', async () => {
createComponent();
await waitForPromises();
findPackageFiles().vm.$emit('delete-file', fileToDelete);
await nextTick();
expect(findDeleteFileModal().exists()).toBe(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
createComponent({ resolver });
await waitForPromises();
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
}),
);
// we are re-fetching the package details, so we expect the resolver to have been called twice
expect(resolver).toHaveBeenCalledTimes(2);
});
describe('errors', () => {
it('shows an error when the mutation request fails', async () => {
createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() });
await waitForPromises();
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
);
});
it('shows an error when the mutation request returns an error payload', async () => {
createComponent({
fileDeleteMutationResolver: jest
.fn()
.mockResolvedValue(packageDestroyFileMutationError()),
});
await waitForPromises();
await doDeleteFile();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
);
});
});
});
});
}); });
import { GlDropdown, GlButton } from '@gitlab/ui'; import { GlDropdown, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import { npmFiles, mavenFiles } from 'jest/packages/mock_data'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import component from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Package Files', () => { describe('Package Files', () => {
let wrapper; let wrapper;
const findAllRows = () => wrapper.findAll('[data-testid="file-row"'); const findAllRows = () => wrapper.findAllByTestId('file-row');
const findFirstRow = () => findAllRows().at(0); const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => findAllRows().at(1); const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]'); const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link');
const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]'); const findFirstRowCommitLink = () => findFirstRow().findByTestId('commit-link');
const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]'); const findSecondRowCommitLink = () => findSecondRow().findByTestId('commit-link');
const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown); const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown));
const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]'); const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
const findFirstRowShaComponent = (id) => wrapper.find(`[data-testid="${id}"]`); const findFirstRowShaComponent = (id) => wrapper.findByTestId(id);
const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => { const files = packageFilesMock();
wrapper = mount(component, { const [file] = files;
const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => {
wrapper = mountExtended(PackageFiles, {
provide: { canDelete },
propsData: { propsData: {
packageFiles, packageFiles,
canDelete,
}, },
stubs: { stubs: {
...stubChildren(component), ...stubChildren(PackageFiles),
GlTable: false, GlTable: false,
}, },
}); });
...@@ -38,7 +41,6 @@ describe('Package Files', () => { ...@@ -38,7 +41,6 @@ describe('Package Files', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('rows', () => { describe('rows', () => {
...@@ -49,7 +51,7 @@ describe('Package Files', () => { ...@@ -49,7 +51,7 @@ describe('Package Files', () => {
}); });
it('renders multiple files for a package that contains more than one file', () => { it('renders multiple files for a package that contains more than one file', () => {
createComponent({ packageFiles: mavenFiles }); createComponent({ packageFiles: files });
expect(findAllRows()).toHaveLength(2); expect(findAllRows()).toHaveLength(2);
}); });
...@@ -65,7 +67,7 @@ describe('Package Files', () => { ...@@ -65,7 +67,7 @@ describe('Package Files', () => {
it('has the correct attrs bound', () => { it('has the correct attrs bound', () => {
createComponent(); createComponent();
expect(findFirstRowDownloadLink().attributes('href')).toBe(npmFiles[0].download_path); expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath);
}); });
it('emits "download-file" event on click', () => { it('emits "download-file" event on click', () => {
...@@ -87,7 +89,7 @@ describe('Package Files', () => { ...@@ -87,7 +89,7 @@ describe('Package Files', () => {
it('has the correct props bound', () => { it('has the correct props bound', () => {
createComponent(); createComponent();
expect(findFirstRowFileIcon().props('fileName')).toBe(npmFiles[0].file_name); expect(findFirstRowFileIcon().props('fileName')).toBe(file.fileName);
}); });
}); });
...@@ -101,35 +103,47 @@ describe('Package Files', () => { ...@@ -101,35 +103,47 @@ describe('Package Files', () => {
it('has the correct props bound', () => { it('has the correct props bound', () => {
createComponent(); createComponent();
expect(findFirstRowCreatedAt().props('time')).toBe(npmFiles[0].created_at); expect(findFirstRowCreatedAt().props('time')).toBe(file.createdAt);
}); });
}); });
describe('commit', () => { describe('commit', () => {
const withPipeline = {
...file,
pipelines: [
{
sha: 'sha',
id: 1,
commitPath: 'commitPath',
},
],
};
describe('when package file has a pipeline associated', () => { describe('when package file has a pipeline associated', () => {
it('exists', () => { it('exists', () => {
createComponent(); createComponent({ packageFiles: [withPipeline] });
expect(findFirstRowCommitLink().exists()).toBe(true); expect(findFirstRowCommitLink().exists()).toBe(true);
}); });
it('the link points to the commit url', () => { it('the link points to the commit path', () => {
createComponent(); createComponent({ packageFiles: [withPipeline] });
expect(findFirstRowCommitLink().attributes('href')).toBe( expect(findFirstRowCommitLink().attributes('href')).toBe(
npmFiles[0].pipelines[0].project.commit_url, withPipeline.pipelines[0].commitPath,
); );
}); });
it('the text is git_commit_message', () => { it('the text is the pipeline sha', () => {
createComponent(); createComponent({ packageFiles: [withPipeline] });
expect(findFirstRowCommitLink().text()).toBe(npmFiles[0].pipelines[0].git_commit_message); expect(findFirstRowCommitLink().text()).toBe(withPipeline.pipelines[0].sha);
}); });
}); });
describe('when package file has no pipeline associated', () => { describe('when package file has no pipeline associated', () => {
it('does not exist', () => { it('does not exist', () => {
createComponent({ packageFiles: mavenFiles }); createComponent();
expect(findFirstRowCommitLink().exists()).toBe(false); expect(findFirstRowCommitLink().exists()).toBe(false);
}); });
...@@ -137,7 +151,7 @@ describe('Package Files', () => { ...@@ -137,7 +151,7 @@ describe('Package Files', () => {
describe('when only one file lacks an associated pipeline', () => { describe('when only one file lacks an associated pipeline', () => {
it('renders the commit when it exists and not otherwise', () => { it('renders the commit when it exists and not otherwise', () => {
createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] }); createComponent({ packageFiles: [withPipeline, file] });
expect(findFirstRowCommitLink().exists()).toBe(true); expect(findFirstRowCommitLink().exists()).toBe(true);
expect(findSecondRowCommitLink().exists()).toBe(false); expect(findSecondRowCommitLink().exists()).toBe(false);
...@@ -166,7 +180,7 @@ describe('Package Files', () => { ...@@ -166,7 +180,7 @@ describe('Package Files', () => {
findActionMenuDelete().vm.$emit('click'); findActionMenuDelete().vm.$emit('click');
const [[{ id }]] = wrapper.emitted('delete-file'); const [[{ id }]] = wrapper.emitted('delete-file');
expect(id).toBe(npmFiles[0].id); expect(id).toBe(file.id);
}); });
}); });
}); });
...@@ -193,10 +207,10 @@ describe('Package Files', () => { ...@@ -193,10 +207,10 @@ describe('Package Files', () => {
}); });
it('is hidden when no details is present', () => { it('is hidden when no details is present', () => {
const [{ ...noShaFile }] = npmFiles; const { ...noShaFile } = file;
noShaFile.file_sha256 = null; noShaFile.fileSha256 = null;
noShaFile.file_md5 = null; noShaFile.fileMd5 = null;
noShaFile.file_sha1 = null; noShaFile.fileSha1 = null;
createComponent({ packageFiles: [noShaFile] }); createComponent({ packageFiles: [noShaFile] });
expect(findFirstToggleDetailsButton().exists()).toBe(false); expect(findFirstToggleDetailsButton().exists()).toBe(false);
...@@ -229,9 +243,9 @@ describe('Package Files', () => { ...@@ -229,9 +243,9 @@ describe('Package Files', () => {
it.each` it.each`
selector | title | sha selector | title | sha
${'sha-256'} | ${'SHA-256'} | ${'file_sha256'} ${'sha-256'} | ${'SHA-256'} | ${'fileSha256'}
${'md5'} | ${'MD5'} | ${'file_md5'} ${'md5'} | ${'MD5'} | ${'fileMd5'}
${'sha-1'} | ${'SHA-1'} | ${'file_sha1'} ${'sha-1'} | ${'SHA-1'} | ${'be93151dc23ac34a82752444556fe79b32c7a1ad'}
`('has a $title row', async ({ selector, title, sha }) => { `('has a $title row', async ({ selector, title, sha }) => {
createComponent(); createComponent();
...@@ -244,8 +258,8 @@ describe('Package Files', () => { ...@@ -244,8 +258,8 @@ describe('Package Files', () => {
}); });
it('does not display a row when the data is missing', async () => { it('does not display a row when the data is missing', async () => {
const [{ ...missingMd5 }] = npmFiles; const { ...missingMd5 } = file;
missingMd5.file_md5 = null; missingMd5.fileMd5 = null;
createComponent({ packageFiles: [missingMd5] }); createComponent({ packageFiles: [missingMd5] });
......
...@@ -29,11 +29,13 @@ export const packagePipelines = (extend) => [ ...@@ -29,11 +29,13 @@ export const packagePipelines = (extend) => [
export const packageFiles = () => [ export const packageFiles = () => [
{ {
id: 'gid://gitlab/Packages::PackageFile/118', id: 'gid://gitlab/Packages::PackageFile/118',
fileMd5: null, fileMd5: 'fileMd5',
fileName: 'foo-1.0.1.tgz', fileName: 'foo-1.0.1.tgz',
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad', fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad',
fileSha256: null, fileSha256: 'fileSha256',
size: '409600', size: '409600',
createdAt: '2020-08-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile', __typename: 'PackageFile',
}, },
{ {
...@@ -43,6 +45,8 @@ export const packageFiles = () => [ ...@@ -43,6 +45,8 @@ export const packageFiles = () => [
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss', fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss',
fileSha256: null, fileSha256: null,
size: '409600', size: '409600',
createdAt: '2020-08-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile', __typename: 'PackageFile',
}, },
]; ];
...@@ -90,7 +94,7 @@ export const nugetMetadata = () => ({ ...@@ -90,7 +94,7 @@ export const nugetMetadata = () => ({
projectUrl: 'projectUrl', projectUrl: 'projectUrl',
}); });
export const packageDetailsQuery = () => ({ export const packageDetailsQuery = (extendPackage) => ({
data: { data: {
package: { package: {
...packageData(), ...packageData(),
...@@ -114,6 +118,7 @@ export const packageDetailsQuery = () => ({ ...@@ -114,6 +118,7 @@ export const packageDetailsQuery = () => ({
__typename: 'PackageFileConnection', __typename: 'PackageFileConnection',
}, },
__typename: 'PackageDetailsType', __typename: 'PackageDetailsType',
...extendPackage,
}, },
}, },
}); });
...@@ -133,6 +138,7 @@ export const packageDestroyMutation = () => ({ ...@@ -133,6 +138,7 @@ export const packageDestroyMutation = () => ({
}, },
}, },
}); });
export const packageDestroyMutationError = () => ({ export const packageDestroyMutationError = () => ({
data: { data: {
destroyPackage: null, destroyPackage: null,
...@@ -151,3 +157,29 @@ export const packageDestroyMutationError = () => ({ ...@@ -151,3 +157,29 @@ export const packageDestroyMutationError = () => ({
}, },
], ],
}); });
export const packageDestroyFileMutation = () => ({
data: {
destroyPackageFile: {
errors: [],
},
},
});
export const packageDestroyFileMutationError = () => ({
data: {
destroyPackageFile: null,
},
errors: [
{
message:
"The resource that you are attempting to access does not exist or you don't have permission to perform this action",
locations: [
{
line: 2,
column: 3,
},
],
path: ['destroyPackageFile'],
},
],
});
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