summaryrefslogtreecommitdiff
path: root/spec/frontend/packages_and_registries/package_registry
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/packages_and_registries/package_registry')
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap29
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap196
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js192
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js187
9 files changed, 606 insertions, 110 deletions
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index fdddc131412..61923233d2e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -29,19 +29,25 @@ exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <span
+ <div
+ class="gl-display-flex gl-gap-3"
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
- class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
- </span>
+
+ <package-tags-stub
+ hidelabel="true"
+ tagdisplaylimit="2"
+ tags="[object Object],[object Object],[object Object]"
+ />
+ </div>
</div>
</div>
</div>
@@ -73,15 +79,6 @@ exports[`PackageTitle renders with tags 1`] = `
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>
@@ -121,19 +118,21 @@ exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <span
+ <div
+ class="gl-display-flex gl-gap-3"
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
- class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
- </span>
+
+ <!---->
+ </div>
</div>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 06ae8645101..92c2cd90568 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -2,19 +2,161 @@
exports[`PypiInstallation renders all the messages 1`] = `
<div>
- <installation-title-stub
- options="[object Object]"
- packagetype="pypi"
- />
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown btn-group"
+ id="__BVID__27"
+ lazy=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
+ id="__BVID__27__BV_toggle_"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-new-dropdown-button-text"
+ >
+ Show PyPi commands
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ aria-labelledby="__BVID__27__BV_toggle_"
+ class="dropdown-menu"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+ </div>
+ </div>
- <code-instruction-stub
- copytext="Copy Pip command"
- data-testid="pip-command"
- instruction="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
- label="Pip Command"
- trackingaction="copy_pip_install_command"
- trackinglabel="code_instruction"
- />
+ <fieldset
+ aria-describedby="installation-pip-command-group__BV_description_"
+ class="form-group gl-form-group"
+ id="installation-pip-command-group"
+ >
+ <legend
+ class="bv-no-focus-ring col-form-label pt-0 col-form-label"
+ id="installation-pip-command-group__BV_label_"
+ tabindex="-1"
+ >
+
+
+
+ <!---->
+
+ <!---->
+ </legend>
+ <div
+ aria-labelledby="installation-pip-command-group__BV_label_"
+ class="bv-no-focus-ring"
+ role="group"
+ tabindex="-1"
+ >
+ <div
+ data-testid="pip-command"
+ id="installation-pip-command"
+ >
+ <label
+ for="instruction-input_5"
+ >
+ Pip Command
+ </label>
+
+ <div
+ class="gl-mb-3"
+ >
+ <div
+ class="input-group gl-mb-3"
+ >
+ <input
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ id="instruction-input_5"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ data-testid="instruction-button"
+ >
+ <button
+ aria-label="Copy Pip command"
+ aria-live="polite"
+ class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-handle-tooltip="false"
+ data-clipboard-text="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
+ id="clipboard-button-6"
+ title="Copy Pip command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ role="img"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ <!---->
+ <!---->
+ <small
+ class="form-text text-muted"
+ id="installation-pip-command-group__BV_description_"
+ tabindex="-1"
+ >
+ You will need a
+ <a
+ class="gl-link"
+ data-testid="access-token-link"
+ href="/help/user/profile/personal_access_tokens"
+ >
+ personal access token
+ </a>
+ .
+ </small>
+ </div>
+ </fieldset>
<h3
class="gl-font-lg"
@@ -30,25 +172,33 @@ exports[`PypiInstallation renders all the messages 1`] = `
file.
</p>
- <code-instruction-stub
- copytext="Copy .pypirc content"
+ <div
data-testid="pypi-setup-content"
- instruction="[gitlab]
+ >
+ <!---->
+
+ <div>
+ <pre
+ class="gl-font-monospace"
+ data-testid="multiline-instruction"
+ >
+ [gitlab]
repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi
username = __token__
-password = <your personal access token>"
- label=""
- multiline="true"
- trackingaction="copy_pypi_setup_command"
- trackinglabel="code_instruction"
- />
+password = &lt;your personal access token&gt;
+ </pre>
+ </div>
+ </div>
For more information on the PyPi registry,
- <gl-link-stub
+ <a
+ class="gl-link"
+ data-testid="pypi-docs-link"
href="/help/user/packages/pypi_repository/index"
+ rel="noopener"
target="_blank"
>
see the documentation
- </gl-link-stub>
+ </a>
.
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 0447ead0830..529a6a22ddf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -1,6 +1,6 @@
-import { GlDropdown, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
-import stubChildren from 'helpers/stub_children';
+import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
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';
@@ -11,6 +11,7 @@ describe('Package Files', () => {
let wrapper;
const findAllRows = () => wrapper.findAllByTestId('file-row');
+ const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected');
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link');
@@ -22,19 +23,27 @@ describe('Package Files', () => {
const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
const findFirstRowShaComponent = (id) => wrapper.findByTestId(id);
+ const findCheckAllCheckbox = () => wrapper.findByTestId('package-files-checkbox-all');
+ const findAllRowCheckboxes = () => wrapper.findAllByTestId('package-files-checkbox');
const files = packageFilesMock();
const [file] = files;
- const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => {
+ const createComponent = ({
+ packageFiles = [file],
+ isLoading = false,
+ canDelete = true,
+ stubs,
+ } = {}) => {
wrapper = mountExtended(PackageFiles, {
propsData: {
canDelete,
+ isLoading,
packageFiles,
},
stubs: {
- ...stubChildren(PackageFiles),
- GlTableLite: false,
+ GlTable: false,
+ ...stubs,
},
});
};
@@ -157,43 +166,170 @@ describe('Package Files', () => {
expect(findSecondRowCommitLink().exists()).toBe(false);
});
});
+ });
- describe('action menu', () => {
- describe('when the user can delete', () => {
- it('exists', () => {
- createComponent();
+ describe('action menu', () => {
+ describe('when the user can delete', () => {
+ it('exists', () => {
+ createComponent();
- expect(findFirstActionMenu().exists()).toBe(true);
- });
+ expect(findFirstActionMenu().exists()).toBe(true);
+ expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v');
+ expect(findFirstActionMenu().props('textSrOnly')).toBe(true);
+ expect(findFirstActionMenu().props('text')).toMatchInterpolatedText('More actions');
+ });
- describe('menu items', () => {
- describe('delete file', () => {
- it('exists', () => {
- createComponent();
+ describe('menu items', () => {
+ describe('delete file', () => {
+ it('exists', () => {
+ createComponent();
- expect(findActionMenuDelete().exists()).toBe(true);
- });
+ expect(findActionMenuDelete().exists()).toBe(true);
+ });
- it('emits a delete event when clicked', () => {
- createComponent();
+ it('emits a delete event when clicked', async () => {
+ createComponent();
- findActionMenuDelete().vm.$emit('click');
+ await findActionMenuDelete().trigger('click');
- const [[{ id }]] = wrapper.emitted('delete-file');
- expect(id).toBe(file.id);
- });
+ const [[items]] = wrapper.emitted('delete-files');
+ const [{ id }] = items;
+ expect(id).toBe(file.id);
});
});
});
+ });
+
+ describe('when the user can not delete', () => {
+ const canDelete = false;
+
+ it('does not exist', () => {
+ createComponent({ canDelete });
+
+ expect(findFirstActionMenu().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('multi select', () => {
+ describe('when user can delete', () => {
+ it('delete selected button exists & is disabled', () => {
+ createComponent();
+
+ expect(findDeleteSelectedButton().exists()).toBe(true);
+ expect(findDeleteSelectedButton().text()).toMatchInterpolatedText('Delete selected');
+ expect(findDeleteSelectedButton().props('disabled')).toBe(true);
+ });
+
+ it('delete selected button exists & is disabled when isLoading prop is true', () => {
+ createComponent({ isLoading: true });
+
+ expect(findDeleteSelectedButton().props('disabled')).toBe(true);
+ });
+
+ it('checkboxes to select file are visible', () => {
+ createComponent({ packageFiles: files });
+
+ expect(findCheckAllCheckbox().exists()).toBe(true);
+ expect(findAllRowCheckboxes()).toHaveLength(2);
+ });
+
+ it('selecting a checkbox enables delete selected button', async () => {
+ createComponent();
+
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ expect(findDeleteSelectedButton().props('disabled')).toBe(false);
+ });
+
+ describe('select all checkbox', () => {
+ it('will toggle between selecting all and deselecting all files', async () => {
+ const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true);
+
+ createComponent({ packageFiles: files });
+
+ expect(getChecked()).toHaveLength(0);
+
+ await findCheckAllCheckbox().setChecked(true);
- describe('when the user can not delete', () => {
- const canDelete = false;
+ expect(getChecked()).toHaveLength(files.length);
- it('does not exist', () => {
- createComponent({ canDelete });
+ await findCheckAllCheckbox().setChecked(false);
- expect(findFirstActionMenu().exists()).toBe(false);
+ expect(getChecked()).toHaveLength(0);
});
+
+ it('will toggle the indeterminate state when some but not all files are selected', async () => {
+ const expectIndeterminateState = (state) =>
+ expect(findCheckAllCheckbox().props('indeterminate')).toBe(state);
+
+ createComponent({
+ packageFiles: files,
+ stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) },
+ });
+
+ expectIndeterminateState(false);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(true);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(false);
+
+ findCheckAllCheckbox().trigger('click');
+
+ expectIndeterminateState(false);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(true);
+ });
+ });
+
+ it('emits a delete event when selected', async () => {
+ createComponent();
+
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ const [[items]] = wrapper.emitted('delete-files');
+ const [{ id }] = items;
+ expect(id).toBe(file.id);
+ });
+
+ it('emits delete event with both items when all are selected', async () => {
+ createComponent({ packageFiles: files });
+
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ const [[items]] = wrapper.emitted('delete-files');
+ expect(items).toHaveLength(2);
+ });
+ });
+
+ describe('when user cannot delete', () => {
+ const canDelete = false;
+
+ it('delete selected button does not exist', () => {
+ createComponent({ canDelete });
+
+ expect(findDeleteSelectedButton().exists()).toBe(false);
+ });
+
+ it('checkboxes to select file are not visible', () => {
+ createComponent({ packageFiles: files, canDelete });
+
+ expect(findCheckAllCheckbox().exists()).toBe(false);
+ expect(findAllRowCheckboxes()).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index f4e6d43812d..ec2e833552a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -17,6 +17,12 @@ import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import waitForPromises from 'helpers/wait_for_promises';
import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql';
+import Tracking from '~/tracking';
+import {
+ TRACKING_ACTION_CLICK_PIPELINE_LINK,
+ TRACKING_ACTION_CLICK_COMMIT_LINK,
+ TRACKING_LABEL_PACKAGE_HISTORY,
+} from '~/packages_and_registries/package_registry/constants';
Vue.use(VueApollo);
@@ -181,7 +187,6 @@ describe('Package History', () => {
it('link', () => {
const linkElement = findElementLink(element);
const exist = Boolean(link);
-
expect(linkElement.exists()).toBe(exist);
if (exist) {
expect(linkElement.attributes('href')).toBe(link);
@@ -189,4 +194,29 @@ describe('Package History', () => {
});
},
);
+ describe('tracking', () => {
+ let eventSpy;
+ const category = 'UI::Packages';
+
+ beforeEach(() => {
+ mountComponent();
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('clicking pipeline link tracks the right action', () => {
+ wrapper.vm.trackPipelineClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_PIPELINE_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+
+ it('clicking commit link tracks the right action', () => {
+ wrapper.vm.trackCommitClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_COMMIT_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index d306f7834f0..37416dcd4e7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -22,16 +22,21 @@ const packageWithTags = {
packageFiles: { nodes: packageFiles() },
};
+const defaultProvide = {
+ isGroupPage: false,
+};
+
describe('PackageTitle', () => {
let wrapper;
- async function createComponent(packageEntity = packageWithTags) {
+ async function createComponent(packageEntity = packageWithTags, provide = defaultProvide) {
wrapper = shallowMountExtended(PackageTitle, {
propsData: { packageEntity },
stubs: {
TitleArea,
GlSprintf,
},
+ provide,
directives: {
GlResizeObserver: createMockDirective(),
},
@@ -199,11 +204,22 @@ describe('PackageTitle', () => {
expect(findPipelineProject().exists()).toBe(false);
});
- it('correctly shows the pipeline project if there is one', async () => {
+ it('does not display the pipeline project on project page even if it exists', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines() },
});
+ expect(findPipelineProject().exists()).toBe(false);
+ });
+
+ it('correctly shows the pipeline project on group page if there is one', async () => {
+ await createComponent(
+ {
+ ...packageData(),
+ pipelines: { nodes: packagePipelines() },
+ },
+ { isGroupPage: true },
+ );
expect(findPipelineProject().props()).toMatchObject({
text: packagePipelines()[0].project.name,
icon: 'review-list',
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index f2fef6436a6..20acb0872e5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -1,9 +1,10 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlSprintf } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
import PypiInstallation from '~/packages_and_registries/package_registry/components/details/pypi_installation.vue';
import {
+ PERSONAL_ACCESS_TOKEN_HELP_URL,
PACKAGE_TYPE_PYPI,
TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
@@ -24,11 +25,12 @@ password = <your personal access token>`;
const pipCommand = () => wrapper.findByTestId('pip-command');
const setupInstruction = () => wrapper.findByTestId('pypi-setup-content');
+ const findAccessTokenLink = () => wrapper.findByTestId('access-token-link');
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
- const findSetupDocsLink = () => wrapper.findComponent(GlLink);
+ const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link');
function createComponent() {
- wrapper = shallowMountExtended(PypiInstallation, {
+ wrapper = mountExtended(PypiInstallation, {
propsData: {
packageEntity,
},
@@ -78,6 +80,12 @@ password = <your personal access token>`;
});
});
+ it('has a link to personal access token docs', () => {
+ expect(findAccessTokenLink().attributes()).toMatchObject({
+ href: PERSONAL_ACCESS_TOKEN_HELP_URL,
+ });
+ });
+
it('has a link to the docs', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: PYPI_HELP_PATH,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index c16c09b5326..eb1e76377ff 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -123,7 +123,7 @@ describe('packages_list_row', () => {
findDeleteDropdown().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index d40feee582f..22236424e6a 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -141,6 +141,7 @@ export const packageData = (extend) => ({
});
export const conanMetadata = () => ({
+ __typename: 'ConanMetadata',
id: 'conan-1',
packageChannel: 'stable',
packageUsername: 'gitlab-org+gitlab-test',
@@ -148,9 +149,8 @@ export const conanMetadata = () => ({
recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable',
});
-const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' });
-
export const composerMetadata = () => ({
+ __typename: 'ComposerMetadata',
targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
composerJson: {
license: 'MIT',
@@ -158,19 +158,14 @@ export const composerMetadata = () => ({
},
});
-const composerMetadataQuery = () => ({
- ...composerMetadata(),
- __typename: 'ComposerMetadata',
-});
-
export const pypiMetadata = () => ({
+ __typename: 'PypiMetadata',
id: 'pypi-1',
requiredPython: '1.0.0',
});
-const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' });
-
export const mavenMetadata = () => ({
+ __typename: 'MavenMetadata',
id: 'maven-1',
appName: 'appName',
appGroup: 'appGroup',
@@ -178,23 +173,20 @@ export const mavenMetadata = () => ({
path: 'path',
});
-const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' });
-
export const nugetMetadata = () => ({
+ __typename: 'NugetMetadata',
id: 'nuget-1',
iconUrl: 'iconUrl',
licenseUrl: 'licenseUrl',
projectUrl: 'projectUrl',
});
-const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' });
-
const packageTypeMetadataQueryMapping = {
- CONAN: conanMetadataQuery,
- COMPOSER: composerMetadataQuery,
- PYPI: pypiMetadataQuery,
- MAVEN: mavenMetadataQuery,
- NUGET: nugetMetadataQuery,
+ CONAN: conanMetadata,
+ COMPOSER: composerMetadata,
+ PYPI: pypiMetadata,
+ MAVEN: mavenMetadata,
+ NUGET: nugetMetadata,
};
export const pagination = (extend) => ({
@@ -221,6 +213,7 @@ export const packageDetailsQuery = (extendPackage) => ({
id: '1',
path: 'projectPath',
name: 'gitlab-test',
+ fullPath: 'gitlab-test',
},
tags: {
nodes: packageTags(),
@@ -231,6 +224,9 @@ export const packageDetailsQuery = (extendPackage) => ({
__typename: 'PipelineConnection',
},
packageFiles: {
+ pageInfo: {
+ hasNextPage: true,
+ },
nodes: packageFiles(),
__typename: 'PackageFileConnection',
},
@@ -310,16 +306,16 @@ export const packageDestroyMutationError = () => ({
],
});
-export const packageDestroyFileMutation = () => ({
+export const packageDestroyFilesMutation = () => ({
data: {
- destroyPackageFile: {
+ destroyPackageFiles: {
errors: [],
},
},
});
-export const packageDestroyFileMutationError = () => ({
+export const packageDestroyFilesMutationError = () => ({
data: {
- destroyPackageFile: null,
+ destroyPackageFiles: null,
},
errors: [
{
@@ -331,7 +327,7 @@ export const packageDestroyFileMutationError = () => ({
column: 3,
},
],
- path: ['destroyPackageFile'],
+ path: ['destroyPackageFiles'],
},
],
});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 3cadb001c58..de78e6bb87b 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -22,6 +22,8 @@ import {
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_CONAN,
@@ -29,7 +31,7 @@ import {
PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
+import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import {
packageDetailsQuery,
@@ -38,8 +40,8 @@ import {
dependencyLinks,
emptyPackageDetailsQuery,
packageFiles,
- packageDestroyFileMutation,
- packageDestroyFileMutationError,
+ packageDestroyFilesMutation,
+ packageDestroyFilesMutationError,
} from '../mock_data';
jest.mock('~/flash');
@@ -58,6 +60,7 @@ describe('PackagesApp', () => {
emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ isGroupPage: false,
breadCrumbState,
};
@@ -65,14 +68,14 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
Vue.use(VueApollo);
const requestHandlers = [
[getPackageDetails, resolver],
- [destroyPackageFileMutation, fileDeleteMutationResolver],
+ [destroyPackageFilesMutation, filesDeleteMutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -110,6 +113,7 @@ describe('PackagesApp', () => {
const findDeleteButton = () => wrapper.findByTestId('delete-package');
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
+ const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionRows = () => wrapper.findAllComponents(VersionRow);
const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
@@ -288,6 +292,7 @@ describe('PackagesApp', () => {
expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy);
+ expect(findPackageFiles().props('isLoading')).toEqual(false);
});
it('does not render the package files table when the package is composer', async () => {
@@ -305,24 +310,69 @@ describe('PackagesApp', () => {
describe('deleting a file', () => {
const [fileToDelete] = packageFiles();
- const doDeleteFile = () => {
- findPackageFiles().vm.$emit('delete-file', fileToDelete);
+ const doDeleteFile = async () => {
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
findDeleteFileModal().vm.$emit('primary');
return waitForPromises();
};
- it('opens a confirmation modal', async () => {
+ it('opens delete file confirmation modal', async () => {
createComponent();
await waitForPromises();
- findPackageFiles().vm.$emit('delete-file', fileToDelete);
+ const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ expect(showDeletePackageSpy).not.toBeCalled();
+ expect(showDeleteFileSpy).toBeCalled();
+ });
+
+ it('when its the only file opens delete package confirmation modal', async () => {
+ const [packageFile] = packageFiles();
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: [packageFile],
+ __typename: 'PackageFileConnection',
+ },
+ }),
+ );
+
+ createComponent({
+ resolver,
+ });
+
+ await waitForPromises();
+
+ const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ expect(showDeletePackageSpy).toBeCalled();
+ expect(showDeleteFileSpy).not.toBeCalled();
+ });
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ findDeleteFileModal().vm.$emit('primary');
await nextTick();
- expect(findDeleteFileModal().exists()).toBe(true);
+ expect(findPackageFiles().props('isLoading')).toEqual(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -344,7 +394,7 @@ describe('PackagesApp', () => {
describe('errors', () => {
it('shows an error when the mutation request fails', async () => {
- createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
await waitForPromises();
await doDeleteFile();
@@ -358,9 +408,9 @@ describe('PackagesApp', () => {
it('shows an error when the mutation request returns an error payload', async () => {
createComponent({
- fileDeleteMutationResolver: jest
+ filesDeleteMutationResolver: jest
.fn()
- .mockResolvedValue(packageDestroyFileMutationError()),
+ .mockResolvedValue(packageDestroyFilesMutationError()),
});
await waitForPromises();
@@ -374,6 +424,117 @@ describe('PackagesApp', () => {
});
});
});
+
+ describe('deleting multiple files', () => {
+ const doDeleteFiles = async () => {
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ findDeleteFilesModal().vm.$emit('primary');
+
+ return waitForPromises();
+ };
+
+ it('opens delete files confirmation modal', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ expect(showDeleteFilesSpy).toBeCalled();
+ });
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ findDeleteFilesModal().vm.$emit('primary');
+
+ await nextTick();
+
+ expect(findPackageFiles().props('isLoading')).toEqual(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 doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_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({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('deleting all files', () => {
+ it('opens the delete package confirmation modal', async () => {
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: packageFiles(),
+ },
+ }),
+ );
+ createComponent({
+ resolver,
+ });
+
+ await waitForPromises();
+
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ expect(showDeletePackageSpy).toBeCalled();
+ });
+ });
});
describe('versions', () => {