426 lines
13 KiB
JavaScript
426 lines
13 KiB
JavaScript
import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
|
|
import Vue, { nextTick } from 'vue';
|
|
|
|
import VueApollo from 'vue-apollo';
|
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
|
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|
import waitForPromises from 'helpers/wait_for_promises';
|
|
import createFlash from '~/flash';
|
|
|
|
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
|
|
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
|
|
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.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 PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
|
|
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
|
|
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
|
|
import {
|
|
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
|
|
PACKAGE_TYPE_COMPOSER,
|
|
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
|
|
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
|
|
PACKAGE_TYPE_NUGET,
|
|
} from '~/packages_and_registries/package_registry/constants';
|
|
|
|
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 {
|
|
packageDetailsQuery,
|
|
packageData,
|
|
packageVersions,
|
|
dependencyLinks,
|
|
emptyPackageDetailsQuery,
|
|
packageFiles,
|
|
packageDestroyFileMutation,
|
|
packageDestroyFileMutationError,
|
|
} from '../mock_data';
|
|
|
|
jest.mock('~/flash');
|
|
useMockLocationHelper();
|
|
|
|
describe('PackagesApp', () => {
|
|
let wrapper;
|
|
let apolloProvider;
|
|
|
|
const breadCrumbState = {
|
|
updateName: jest.fn(),
|
|
};
|
|
|
|
const provide = {
|
|
packageId: '111',
|
|
emptyListIllustration: 'svgPath',
|
|
projectListUrl: 'projectListUrl',
|
|
groupListUrl: 'groupListUrl',
|
|
breadCrumbState,
|
|
};
|
|
|
|
const { __typename, ...packageWithoutTypename } = packageData();
|
|
|
|
function createComponent({
|
|
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
|
|
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
|
|
routeId = '1',
|
|
} = {}) {
|
|
Vue.use(VueApollo);
|
|
|
|
const requestHandlers = [
|
|
[getPackageDetails, resolver],
|
|
[destroyPackageFileMutation, fileDeleteMutationResolver],
|
|
];
|
|
apolloProvider = createMockApollo(requestHandlers);
|
|
|
|
wrapper = shallowMountExtended(PackagesApp, {
|
|
apolloProvider,
|
|
provide,
|
|
stubs: {
|
|
PackageTitle,
|
|
DeletePackage,
|
|
GlModal: {
|
|
template: '<div></div>',
|
|
methods: {
|
|
show: jest.fn(),
|
|
},
|
|
},
|
|
GlTabs,
|
|
GlTab,
|
|
},
|
|
mocks: {
|
|
$route: {
|
|
params: {
|
|
id: routeId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
|
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
|
|
const findPackageHistory = () => wrapper.findComponent(PackageHistory);
|
|
const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata);
|
|
const findInstallationCommands = () => wrapper.findComponent(InstallationCommands);
|
|
const findDeleteModal = () => wrapper.findByTestId('delete-modal');
|
|
const findDeleteButton = () => wrapper.findByTestId('delete-package');
|
|
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
|
|
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
|
|
const findVersionRows = () => wrapper.findAllComponents(VersionRow);
|
|
const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
|
|
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
|
|
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
|
|
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
|
|
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
|
|
|
|
afterEach(() => {
|
|
wrapper.destroy();
|
|
});
|
|
|
|
it('renders an empty state component', async () => {
|
|
createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) });
|
|
|
|
await waitForPromises();
|
|
|
|
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(packageWithoutTypename),
|
|
});
|
|
});
|
|
|
|
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,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('renders history and has the right props', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findPackageHistory().exists()).toBe(true);
|
|
expect(findPackageHistory().props()).toMatchObject({
|
|
packageEntity: expect.objectContaining(packageWithoutTypename),
|
|
projectName: packageDetailsQuery().data.package.project.name,
|
|
});
|
|
});
|
|
|
|
it('renders additional metadata and has the right props', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findAdditionalMetadata().exists()).toBe(true);
|
|
expect(findAdditionalMetadata().props()).toMatchObject({
|
|
packageEntity: expect.objectContaining(packageWithoutTypename),
|
|
});
|
|
});
|
|
|
|
it('renders installation commands and has the right props', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findInstallationCommands().exists()).toBe(true);
|
|
expect(findInstallationCommands().props()).toMatchObject({
|
|
packageEntity: expect.objectContaining(packageWithoutTypename),
|
|
});
|
|
});
|
|
|
|
it('calls the appropriate function to set the breadcrumbState', async () => {
|
|
const { name, version } = packageData();
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`);
|
|
});
|
|
|
|
describe('delete package', () => {
|
|
const originalReferrer = document.referrer;
|
|
const setReferrer = (value = packageDetailsQuery().data.package.project.name) => {
|
|
Object.defineProperty(document, 'referrer', {
|
|
value,
|
|
configurable: true,
|
|
});
|
|
};
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(document, 'referrer', {
|
|
value: originalReferrer,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it('shows the delete confirmation modal when delete is clicked', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
await findDeleteButton().trigger('click');
|
|
|
|
expect(findDeleteModal().exists()).toBe(true);
|
|
});
|
|
|
|
describe('successful request', () => {
|
|
it('when referrer contains project name calls window.replace with project url', async () => {
|
|
setReferrer();
|
|
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
findDeletePackage().vm.$emit('end');
|
|
|
|
expect(window.location.replace).toHaveBeenCalledWith(
|
|
'projectListUrl?showSuccessDeleteAlert=true',
|
|
);
|
|
});
|
|
|
|
it('when referrer does not contain project name calls window.replace with group url', async () => {
|
|
setReferrer('baz');
|
|
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
findDeletePackage().vm.$emit('end');
|
|
|
|
expect(window.location.replace).toHaveBeenCalledWith(
|
|
'groupListUrl?showSuccessDeleteAlert=true',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
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);
|
|
expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy);
|
|
});
|
|
|
|
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,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('versions', () => {
|
|
it('displays the correct version count when the package has versions', async () => {
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findVersionRows()).toHaveLength(packageVersions().length);
|
|
});
|
|
|
|
it('binds the correct props', async () => {
|
|
const [versionPackage] = packageVersions();
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
delete versionPackage.__typename;
|
|
delete versionPackage.tags;
|
|
|
|
createComponent();
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findVersionRows().at(0).props()).toMatchObject({
|
|
packageEntity: expect.objectContaining(versionPackage),
|
|
});
|
|
});
|
|
|
|
it('displays the no versions message when there are none', async () => {
|
|
createComponent({
|
|
resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })),
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(noVersionsMessage().exists()).toBe(true);
|
|
});
|
|
});
|
|
describe('dependency links', () => {
|
|
it('does not show the dependency links for a non nuget package', async () => {
|
|
createComponent();
|
|
|
|
expect(findDependenciesCountBadge().exists()).toBe(false);
|
|
});
|
|
|
|
it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => {
|
|
createComponent({
|
|
resolver: jest.fn().mockResolvedValue(
|
|
packageDetailsQuery({
|
|
packageType: PACKAGE_TYPE_NUGET,
|
|
dependencyLinks: { nodes: [] },
|
|
}),
|
|
),
|
|
});
|
|
|
|
await waitForPromises();
|
|
|
|
expect(findDependenciesCountBadge().exists()).toBe(true);
|
|
expect(findDependenciesCountBadge().text()).toBe('0');
|
|
expect(findNoDependenciesMessage().exists()).toBe(true);
|
|
});
|
|
|
|
it('renders the correct number of dependency rows for a nuget package', async () => {
|
|
createComponent({
|
|
resolver: jest.fn().mockResolvedValue(
|
|
packageDetailsQuery({
|
|
packageType: PACKAGE_TYPE_NUGET,
|
|
}),
|
|
),
|
|
});
|
|
await waitForPromises();
|
|
|
|
expect(findDependenciesCountBadge().exists()).toBe(true);
|
|
expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString());
|
|
expect(findDependencyRows()).toHaveLength(dependencyLinks().length);
|
|
});
|
|
});
|
|
});
|