Commit 9645e8e0 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Natalia Tepluhina

Add new delete_alert component

- new component
- unit tests
parent bb396938
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
export default {
components: {
GlSprintf,
GlAlert,
GlLink,
},
model: {
prop: 'deleteAlertType',
event: 'change',
},
props: {
deleteAlertType: {
type: String,
default: null,
required: false,
validator(value) {
return !value || ALERT_MESSAGES[value] !== undefined;
},
},
garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
isAdmin: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
deleteAlertConfig() {
const config = {
title: '',
message: '',
type: 'success',
};
if (this.deleteAlertType) {
[config.type] = this.deleteAlertType.split('_');
config.message = ALERT_MESSAGES[this.deleteAlertType];
if (this.isAdmin && config.type === 'success') {
config.title = config.message;
config.message = ADMIN_GARBAGE_COLLECTION_TIP;
}
}
return config;
},
},
};
</script>
<template>
<gl-alert
v-if="deleteAlertType"
:variant="deleteAlertConfig.type"
:title="deleteAlertConfig.title"
@dismiss="$emit('change', null)"
>
<gl-sprintf :message="deleteAlertConfig.message">
<template #docLink="{content}">
<gl-link :href="garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
export default {
components: {
GlModal,
GlSprintf,
},
props: {
itemsToBeDeleted: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
modalAction() {
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
this.itemsToBeDeleted.length,
);
},
modalDescription() {
if (this.itemsToBeDeleted.length > 1) {
return {
message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
}
const [first] = this.itemsToBeDeleted;
return {
message: REMOVE_TAG_CONFIRMATION_TEXT,
item: first?.path,
};
},
},
methods: {
show() {
this.$refs.deleteModal.show();
},
},
};
</script>
<template>
<gl-modal
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
@ok="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
>
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<p v-if="modalDescription" data-testid="description">
<gl-sprintf :message="modalDescription.message">
<template #item
><b>{{ modalDescription.item }}</b></template
>
</gl-sprintf>
</p>
</gl-modal>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { DETAILS_PAGE_TITLE } from '../../constants/index';
export default {
components: { GlSprintf },
props: {
imageName: {
type: String,
required: false,
default: '',
},
},
i18n: {
DETAILS_PAGE_TITLE,
},
};
</script>
<template>
<div class="gl-display-flex gl-my-2 gl-align-items-center">
<h4>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ imageName }}
</template>
</gl-sprintf>
</h4>
</div>
</template>
...@@ -47,3 +47,14 @@ export const LIST_KEY_SIZE = 'total_size'; ...@@ -47,3 +47,14 @@ export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at'; export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions'; export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox'; export const LIST_KEY_CHECKBOX = 'checkbox';
export const ALERT_SUCCESS_TAG = 'success_tag';
export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE,
[ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE,
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
};
...@@ -7,10 +7,6 @@ import { ...@@ -7,10 +7,6 @@ import {
GlIcon, GlIcon,
GlTooltipDirective, GlTooltipDirective,
GlPagination, GlPagination,
GlModal,
GlSprintf,
GlAlert,
GlLink,
GlEmptyState, GlEmptyState,
GlResizeObserverDirective, GlResizeObserverDirective,
GlSkeletonLoader, GlSkeletonLoader,
...@@ -21,6 +17,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -21,6 +17,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import DeleteAlert from '../components/details_page/delete_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
import { decodeAndParse } from '../utils'; import { decodeAndParse } from '../utils';
import { import {
LIST_KEY_TAG, LIST_KEY_TAG,
...@@ -33,34 +32,29 @@ import { ...@@ -33,34 +32,29 @@ import {
LIST_LABEL_IMAGE_ID, LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE, LIST_LABEL_SIZE,
LIST_LABEL_LAST_UPDATED, LIST_LABEL_LAST_UPDATED,
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE, REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE, EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE, EMPTY_IMAGE_REPOSITORY_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP, ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
} from '../constants/index'; } from '../constants/index';
export default { export default {
components: { components: {
DeleteAlert,
DetailsHeader,
GlTable, GlTable,
GlFormCheckbox, GlFormCheckbox,
GlDeprecatedButton, GlDeprecatedButton,
GlIcon, GlIcon,
ClipboardButton, ClipboardButton,
GlPagination, GlPagination,
GlModal, DeleteModal,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf,
GlEmptyState, GlEmptyState,
GlAlert,
GlLink,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -73,18 +67,11 @@ export default { ...@@ -73,18 +67,11 @@ export default {
height: 40, height: 40,
}, },
i18n: { i18n: {
DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE, REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE, EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE, EMPTY_IMAGE_REPOSITORY_MESSAGE,
}, },
alertMessages: {
success_tag: DELETE_TAG_SUCCESS_MESSAGE,
danger_tag: DELETE_TAG_ERROR_MESSAGE,
success_tags: DELETE_TAGS_SUCCESS_MESSAGE,
danger_tags: DELETE_TAGS_ERROR_MESSAGE,
},
data() { data() {
return { return {
selectedItems: [], selectedItems: [],
...@@ -92,7 +79,7 @@ export default { ...@@ -92,7 +79,7 @@ export default {
selectAllChecked: false, selectAllChecked: false,
modalDescription: null, modalDescription: null,
isDesktop: true, isDesktop: true,
deleteAlertType: false, deleteAlertType: null,
}; };
}, },
computed: { computed: {
...@@ -119,21 +106,12 @@ export default { ...@@ -119,21 +106,12 @@ export default {
{ key: LIST_KEY_ACTIONS, label: '' }, { key: LIST_KEY_ACTIONS, label: '' },
].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop); ].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
}, },
isMultiDelete() {
return this.itemsToBeDeleted.length > 1;
},
tracking() { tracking() {
return { return {
label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete', label:
this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
}; };
}, },
modalAction() {
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
);
},
currentPage: { currentPage: {
get() { get() {
return this.tagsPagination.page; return this.tagsPagination.page;
...@@ -142,47 +120,12 @@ export default { ...@@ -142,47 +120,12 @@ export default {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id }); this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
}, },
}, },
deleteAlertConfig() {
const config = {
title: '',
message: '',
type: 'success',
};
if (this.deleteAlertType) {
[config.type] = this.deleteAlertType.split('_');
const defaultMessage = this.$options.alertMessages[this.deleteAlertType];
if (this.config.isAdmin && config.type === 'success') {
config.title = defaultMessage;
config.message = ADMIN_GARBAGE_COLLECTION_TIP;
} else {
config.message = defaultMessage;
}
}
return config;
},
}, },
mounted() { mounted() {
this.requestTagsList({ params: this.$route.params.id }); this.requestTagsList({ params: this.$route.params.id });
}, },
methods: { methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']), ...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = {
message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
} else {
const { path } = this.tags[itemIndex];
this.modalDescription = {
message: REMOVE_TAG_CONFIRMATION_TEXT,
item: path,
};
}
},
formatSize(size) { formatSize(size) {
return numberToHumanSize(size); return numberToHumanSize(size);
}, },
...@@ -197,53 +140,49 @@ export default { ...@@ -197,53 +140,49 @@ export default {
} }
}, },
selectAll() { selectAll() {
this.selectedItems = this.tags.map((x, index) => index); this.selectedItems = this.tags.map(x => x.name);
this.selectAllChecked = true; this.selectAllChecked = true;
}, },
deselectAll() { deselectAll() {
this.selectedItems = []; this.selectedItems = [];
this.selectAllChecked = false; this.selectAllChecked = false;
}, },
updateSelectedItems(index) { updateSelectedItems(name) {
const delIndex = this.selectedItems.findIndex(x => x === index); const delIndex = this.selectedItems.findIndex(x => x === name);
if (delIndex > -1) { if (delIndex > -1) {
this.selectedItems.splice(delIndex, 1); this.selectedItems.splice(delIndex, 1);
this.selectAllChecked = false; this.selectAllChecked = false;
} else { } else {
this.selectedItems.push(index); this.selectedItems.push(name);
if (this.selectedItems.length === this.tags.length) { if (this.selectedItems.length === this.tags.length) {
this.selectAllChecked = true; this.selectAllChecked = true;
} }
} }
}, },
deleteSingleItem(index) { deleteSingleItem(name) {
this.setModalDescription(index); this.itemsToBeDeleted = [{ ...this.tags.find(t => t.name === name) }];
this.itemsToBeDeleted = [index];
this.track('click_button'); this.track('click_button');
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
deleteMultipleItems() { deleteMultipleItems() {
this.itemsToBeDeleted = [...this.selectedItems]; this.itemsToBeDeleted = this.selectedItems.map(name => ({
if (this.selectedItems.length === 1) { ...this.tags.find(t => t.name === name),
this.setModalDescription(this.itemsToBeDeleted[0]); }));
} else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
this.track('click_button'); this.track('click_button');
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
handleSingleDelete(index) { handleSingleDelete() {
const itemToDelete = this.tags[index]; const [itemToDelete] = this.itemsToBeDeleted;
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.selectedItems = this.selectedItems.filter(i => i !== index); this.selectedItems = this.selectedItems.filter(name => name !== itemToDelete.name);
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id }) return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
.then(() => { .then(() => {
this.deleteAlertType = 'success_tag'; this.deleteAlertType = ALERT_SUCCESS_TAG;
}) })
.catch(() => { .catch(() => {
this.deleteAlertType = 'danger_tag'; this.deleteAlertType = ALERT_DANGER_TAG;
}); });
}, },
handleMultipleDelete() { handleMultipleDelete() {
...@@ -252,22 +191,22 @@ export default { ...@@ -252,22 +191,22 @@ export default {
this.selectedItems = []; this.selectedItems = [];
return this.requestDeleteTags({ return this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name), ids: itemsToBeDeleted.map(x => x.name),
params: this.$route.params.id, params: this.$route.params.id,
}) })
.then(() => { .then(() => {
this.deleteAlertType = 'success_tags'; this.deleteAlertType = ALERT_SUCCESS_TAGS;
}) })
.catch(() => { .catch(() => {
this.deleteAlertType = 'danger_tags'; this.deleteAlertType = ALERT_DANGER_TAGS;
}); });
}, },
onDeletionConfirmed() { onDeletionConfirmed() {
this.track('confirm_delete'); this.track('confirm_delete');
if (this.isMultiDelete) { if (this.itemsToBeDeleted.length > 1) {
this.handleMultipleDelete(); this.handleMultipleDelete();
} else { } else {
this.handleSingleDelete(this.itemsToBeDeleted[0]); this.handleSingleDelete();
} }
}, },
handleResize() { handleResize() {
...@@ -279,30 +218,14 @@ export default { ...@@ -279,30 +218,14 @@ export default {
<template> <template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element"> <div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
<gl-alert <delete-alert
v-if="deleteAlertType" v-model="deleteAlertType"
:variant="deleteAlertConfig.type" :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
:title="deleteAlertConfig.title" :is-admin="config.isAdmin"
class="my-2" class="my-2"
@dismiss="deleteAlertType = null" />
>
<gl-sprintf :message="deleteAlertConfig.message"> <details-header :image-name="imageName" />
<template #docLink="{content}">
<gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<div class="d-flex my-3 align-items-center">
<h4>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ imageName }}
</template>
</gl-sprintf>
</h4>
</div>
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty> <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
<template v-if="isDesktop" #head(checkbox)> <template v-if="isDesktop" #head(checkbox)>
...@@ -327,12 +250,12 @@ export default { ...@@ -327,12 +250,12 @@ export default {
</gl-deprecated-button> </gl-deprecated-button>
</template> </template>
<template #cell(checkbox)="{index}"> <template #cell(checkbox)="{item}">
<gl-form-checkbox <gl-form-checkbox
ref="rowCheckbox" ref="rowCheckbox"
class="js-row-checkbox" class="js-row-checkbox"
:checked="selectedItems.includes(index)" :checked="selectedItems.includes(item.name)"
@change="updateSelectedItems(index)" @change="updateSelectedItems(item.name)"
/> />
</template> </template>
<template #cell(name)="{item, field}"> <template #cell(name)="{item, field}">
...@@ -373,7 +296,7 @@ export default { ...@@ -373,7 +296,7 @@ export default {
{{ timeFormatted(value) }} {{ timeFormatted(value) }}
</span> </span>
</template> </template>
<template #cell(actions)="{index, item}"> <template #cell(actions)="{item}">
<gl-deprecated-button <gl-deprecated-button
ref="singleDeleteButton" ref="singleDeleteButton"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
...@@ -381,7 +304,7 @@ export default { ...@@ -381,7 +304,7 @@ export default {
:disabled="!item.destroy_path" :disabled="!item.destroy_path"
variant="danger" variant="danger"
class="js-delete-registry float-right btn-inverted btn-border-color btn-icon" class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
@click="deleteSingleItem(index)" @click="deleteSingleItem(item.name)"
> >
<gl-icon name="remove" /> <gl-icon name="remove" />
</gl-deprecated-button> </gl-deprecated-button>
...@@ -425,22 +348,11 @@ export default { ...@@ -425,22 +348,11 @@ export default {
class="w-100" class="w-100"
/> />
<gl-modal <delete-modal
ref="deleteModal" ref="deleteModal"
modal-id="delete-tag-modal" :items-to-be-deleted="itemsToBeDeleted"
ok-variant="danger" @confirmDelete="onDeletionConfirmed"
@ok="onDeletionConfirmed"
@cancel="track('cancel_delete')" @cancel="track('cancel_delete')"
> />
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<p v-if="modalDescription">
<gl-sprintf :message="modalDescription.message">
<template #item>
<b>{{ modalDescription.item }}</b>
</template>
</gl-sprintf>
</p>
</gl-modal>
</div> </div>
</template> </template>
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/delete_alert.vue';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/registry/explorer/constants';
describe('Delete alert', () => {
let wrapper;
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const mountComponent = propsData => {
wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
};
describe('when deleteAlertType is null', () => {
it('does not show the alert', () => {
mountComponent();
expect(findAlert().exists()).toBe(false);
});
});
describe('when deleteAlertType is not null', () => {
describe('success states', () => {
describe.each`
deleteAlertType | message
${'success_tag'} | ${DELETE_TAG_SUCCESS_MESSAGE}
${'success_tags'} | ${DELETE_TAGS_SUCCESS_MESSAGE}
`('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => {
it('alert exists', () => {
mountComponent({ deleteAlertType });
expect(findAlert().exists()).toBe(true);
});
describe('when the user is an admin', () => {
beforeEach(() => {
mountComponent({
deleteAlertType,
isAdmin: true,
garbageCollectionHelpPagePath: 'foo',
});
});
it(`alert title is ${message}`, () => {
expect(findAlert().attributes('title')).toBe(message);
});
it('alert body contains admin tip', () => {
expect(findAlert().text()).toMatchInterpolatedText(ADMIN_GARBAGE_COLLECTION_TIP);
});
it('alert body contains link', () => {
const alertLink = findLink();
expect(alertLink.exists()).toBe(true);
expect(alertLink.attributes('href')).toBe('foo');
});
});
describe('when the user is not an admin', () => {
it('alert exist and text is appropriate', () => {
mountComponent({ deleteAlertType });
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(message);
});
});
});
});
describe('error states', () => {
describe.each`
deleteAlertType | message
${'danger_tag'} | ${DELETE_TAG_ERROR_MESSAGE}
${'danger_tags'} | ${DELETE_TAGS_ERROR_MESSAGE}
`('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => {
it('alert exists', () => {
mountComponent({ deleteAlertType });
expect(findAlert().exists()).toBe(true);
});
describe('when the user is an admin', () => {
it('alert exist and text is appropriate', () => {
mountComponent({ deleteAlertType });
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(message);
});
});
describe('when the user is not an admin', () => {
it('alert exist and text is appropriate', () => {
mountComponent({ deleteAlertType });
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(message);
});
});
});
});
describe('dismissing alert', () => {
it('GlAlert dismiss event triggers a change event', () => {
mountComponent({ deleteAlertType: 'success_tags' });
findAlert().vm.$emit('dismiss');
expect(wrapper.emitted('change')).toEqual([[null]]);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/delete_modal.vue';
import {
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
} from '~/registry/explorer/constants';
import { GlModal } from '../../stubs';
describe('Delete Modal', () => {
let wrapper;
const findModal = () => wrapper.find(GlModal);
const findDescription = () => wrapper.find('[data-testid="description"]');
const mountComponent = propsData => {
wrapper = shallowMount(component, {
propsData,
stubs: {
GlSprintf,
GlModal,
},
});
};
it('contains a GlModal', () => {
mountComponent();
expect(findModal().exists()).toBe(true);
});
describe('events', () => {
it.each`
glEvent | localEvent
${'ok'} | ${'confirmDelete'}
${'cancel'} | ${'cancelDelete'}
`('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => {
mountComponent();
findModal().vm.$emit(glEvent);
expect(wrapper.emitted(localEvent)).toBeTruthy();
});
});
describe('methods', () => {
it('show calls gl-modal show', () => {
mountComponent();
wrapper.vm.show();
expect(GlModal.methods.show).toHaveBeenCalled();
});
});
describe('itemsToBeDeleted contains one element', () => {
beforeEach(() => {
mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
});
it(`has the correct description`, () => {
expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'));
});
it('has the correct action', () => {
expect(wrapper.text()).toContain('Remove tag');
});
});
describe('itemsToBeDeleted contains more than element', () => {
beforeEach(() => {
mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
});
it(`has the correct description`, () => {
expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'));
});
it('has the correct action', () => {
expect(wrapper.text()).toContain('Remove tags');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants';
describe('Details Header', () => {
let wrapper;
const mountComponent = propsData => {
wrapper = shallowMount(component, {
propsData,
stubs: {
GlSprintf,
},
});
};
it('has the correct title ', () => {
mountComponent();
expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
it('shows imageName in the title', () => {
mountComponent({ imageName: 'foo' });
expect(wrapper.text()).toContain('foo');
});
});
...@@ -64,7 +64,7 @@ export const imagesListResponse = { ...@@ -64,7 +64,7 @@ export const imagesListResponse = {
export const tagsListResponse = { export const tagsListResponse = {
data: [ data: [
{ {
tag: 'centos6', name: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
short_revision: 'b118ab5b0', short_revision: 'b118ab5b0',
size: 19, size: 19,
...@@ -75,7 +75,7 @@ export const tagsListResponse = { ...@@ -75,7 +75,7 @@ export const tagsListResponse = {
destroy_path: 'path', destroy_path: 'path',
}, },
{ {
tag: 'test-image', name: 'test-tag',
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4', revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
short_revision: 'b969de599', short_revision: 'b969de599',
size: 19, size: 19,
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlTable, GlPagination, GlSkeletonLoader, GlAlert, GlLink } from '@gitlab/ui'; import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue'; import component from '~/registry/explorer/pages/details.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
import DeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import { createStore } from '~/registry/explorer/stores/'; import { createStore } from '~/registry/explorer/stores/';
import { import {
SET_MAIN_LOADING, SET_MAIN_LOADING,
SET_INITIAL_STATE,
SET_TAGS_LIST_SUCCESS, SET_TAGS_LIST_SUCCESS,
SET_TAGS_PAGINATION, SET_TAGS_PAGINATION,
SET_INITIAL_STATE,
} from '~/registry/explorer/stores/mutation_types/'; } from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/registry/explorer/constants';
import { tagsListResponse } from '../mock_data'; import { tagsListResponse } from '../mock_data';
import { GlModal } from '../stubs';
import { $toast } from '../../shared/mocks'; import { $toast } from '../../shared/mocks';
describe('Details Page', () => { describe('Details Page', () => {
...@@ -26,7 +22,7 @@ describe('Details Page', () => { ...@@ -26,7 +22,7 @@ describe('Details Page', () => {
let dispatchSpy; let dispatchSpy;
let store; let store;
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(DeleteModal);
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' }); const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
...@@ -38,7 +34,8 @@ describe('Details Page', () => { ...@@ -38,7 +34,8 @@ describe('Details Page', () => {
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked')); const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column'); const findFirsTagColumn = () => wrapper.find('.js-tag-column');
const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]'); const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
const findAlert = () => wrapper.find(GlAlert); const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' })); const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
...@@ -47,9 +44,9 @@ describe('Details Page', () => { ...@@ -47,9 +44,9 @@ describe('Details Page', () => {
store, store,
stubs: { stubs: {
...stubChildren(component), ...stubChildren(component),
GlModal,
GlSprintf: false, GlSprintf: false,
GlTable, GlTable,
DeleteModal,
}, },
mocks: { mocks: {
$route: { $route: {
...@@ -70,6 +67,7 @@ describe('Details Page', () => { ...@@ -70,6 +67,7 @@ describe('Details Page', () => {
store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data); store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers); store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
jest.spyOn(DeleteModal.methods, 'show');
}); });
afterEach(() => { afterEach(() => {
...@@ -100,10 +98,6 @@ describe('Details Page', () => { ...@@ -100,10 +98,6 @@ describe('Details Page', () => {
}); });
describe('table', () => { describe('table', () => {
beforeEach(() => {
mountComponent();
});
it.each([ it.each([
'rowCheckbox', 'rowCheckbox',
'rowName', 'rowName',
...@@ -112,6 +106,7 @@ describe('Details Page', () => { ...@@ -112,6 +106,7 @@ describe('Details Page', () => {
'rowTime', 'rowTime',
'singleDeleteButton', 'singleDeleteButton',
])('%s exist in the table', element => { ])('%s exist in the table', element => {
mountComponent();
expect(findFirstRowItem(element).exists()).toBe(true); expect(findFirstRowItem(element).exists()).toBe(true);
}); });
...@@ -143,16 +138,20 @@ describe('Details Page', () => { ...@@ -143,16 +138,20 @@ describe('Details Page', () => {
}); });
describe('row checkbox', () => { describe('row checkbox', () => {
beforeEach(() => {
mountComponent();
});
it('if selected adds item to selectedItems', () => { it('if selected adds item to selectedItems', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change'); findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectedItems).toEqual([1]); expect(wrapper.vm.selectedItems).toEqual([store.state.tags[1].name]);
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy(); expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy();
}); });
}); });
it('if deselect remove index from selectedItems', () => { it('if deselect remove name from selectedItems', () => {
wrapper.setData({ selectedItems: [1] }); wrapper.setData({ selectedItems: [store.state.tags[1].name] });
findFirstRowItem('rowCheckbox').vm.$emit('change'); findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectedItems.length).toBe(0); expect(wrapper.vm.selectedItems.length).toBe(0);
...@@ -167,14 +166,17 @@ describe('Details Page', () => { ...@@ -167,14 +166,17 @@ describe('Details Page', () => {
}); });
it('exists', () => { it('exists', () => {
mountComponent();
expect(findBulkDeleteButton().exists()).toBe(true); expect(findBulkDeleteButton().exists()).toBe(true);
}); });
it('is disabled if no item is selected', () => { it('is disabled if no item is selected', () => {
mountComponent();
expect(findBulkDeleteButton().attributes('disabled')).toBe('true'); expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
}); });
it('is enabled if at least one item is selected', () => { it('is enabled if at least one item is selected', () => {
mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) });
wrapper.setData({ selectedItems: [1] }); wrapper.setData({ selectedItems: [1] });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy(); expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
...@@ -183,30 +185,26 @@ describe('Details Page', () => { ...@@ -183,30 +185,26 @@ describe('Details Page', () => {
describe('on click', () => { describe('on click', () => {
it('when one item is selected', () => { it('when one item is selected', () => {
wrapper.setData({ selectedItems: [1] }); mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) });
jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
findBulkDeleteButton().vm.$emit('click'); findBulkDeleteButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.itemsToBeDeleted).toEqual([store.state.tags[0]]);
expect(findDeleteModal().html()).toContain( expect(DeleteModal.methods.show).toHaveBeenCalled();
'You are about to remove <b>foo</b>. Are you sure?', expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
); label: 'registry_tag_delete',
expect(GlModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
}); });
}); });
it('when multiple items are selected', () => { it('when multiple items are selected', () => {
wrapper.setData({ selectedItems: [0, 1] }); mountComponent({
data: () => ({ selectedItems: store.state.tags.map(t => t.name) }),
});
findBulkDeleteButton().vm.$emit('click'); findBulkDeleteButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain( expect(wrapper.vm.itemsToBeDeleted).toEqual(tagsListResponse.data);
'You are about to remove <b>2</b> tags. Are you sure?', expect(DeleteModal.methods.show).toHaveBeenCalled();
); expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
expect(GlModal.methods.show).toHaveBeenCalled(); label: 'bulk_registry_tag_delete',
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'bulk_registry_tag_delete',
});
}); });
}); });
}); });
...@@ -237,14 +235,10 @@ describe('Details Page', () => { ...@@ -237,14 +235,10 @@ describe('Details Page', () => {
findAllDeleteButtons() findAllDeleteButtons()
.at(0) .at(0)
.vm.$emit('click'); .vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain( expect(DeleteModal.methods.show).toHaveBeenCalled();
'You are about to remove <b>bar</b>. Are you sure?', expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
); label: 'registry_tag_delete',
expect(GlModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
}); });
}); });
}); });
...@@ -292,6 +286,7 @@ describe('Details Page', () => { ...@@ -292,6 +286,7 @@ describe('Details Page', () => {
let timeCell; let timeCell;
beforeEach(() => { beforeEach(() => {
mountComponent();
timeCell = findFirstRowItem('rowTime'); timeCell = findFirstRowItem('rowTime');
}); });
...@@ -331,176 +326,97 @@ describe('Details Page', () => { ...@@ -331,176 +326,97 @@ describe('Details Page', () => {
}); });
describe('modal', () => { describe('modal', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => { it('exists', () => {
mountComponent();
expect(findDeleteModal().exists()).toBe(true); expect(findDeleteModal().exists()).toBe(true);
}); });
describe('when ok event is emitted', () => { describe('cancel event', () => {
beforeEach(() => { it('tracks cancel_delete', () => {
dispatchSpy.mockResolvedValue(); mountComponent();
}); findDeleteModal().vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
it('tracks confirm_delete', () => { label: 'registry_tag_delete',
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'confirm_delete', {
label: 'registry_tag_delete',
});
}); });
}); });
});
describe('when only one element is selected', () => { describe('confirmDelete event', () => {
it('execute the delete and remove selection', () => { describe('when one item is selected to be deleted', () => {
wrapper.setData({ itemsToBeDeleted: [0] }); const itemsToBeDeleted = [{ name: 'foo' }];
findDeleteModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', { it('dispatch requestDeleteTag with the right parameters', () => {
tag: store.state.tags[0], mountComponent({ data: () => ({ itemsToBeDeleted }) });
params: wrapper.vm.$route.params.id, findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', {
tag: itemsToBeDeleted[0],
params: routeId,
}); });
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items });
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); it('remove the deleted item from the selected items', () => {
expect(wrapper.vm.selectedItems).toEqual([]); mountComponent({ data: () => ({ itemsToBeDeleted, selectedItems: ['foo', 'bar'] }) });
expect(findCheckedCheckboxes()).toHaveLength(0); findDeleteModal().vm.$emit('confirmDelete');
expect(wrapper.vm.selectedItems).toEqual(['bar']);
}); });
}); });
describe('when multiple elements are selected', () => { describe('when more than one item is selected to be deleted', () => {
beforeEach(() => { beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0, 1] }); mountComponent({
data: () => ({
itemsToBeDeleted: [{ name: 'foo' }, { name: 'bar' }],
selectedItems: ['foo', 'bar'],
}),
});
}); });
it('execute the delete and remove selection', () => { it('dispatch requestDeleteTags with the right parameters', () => {
findDeleteModal().vm.$emit('ok'); findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', { ids: ['foo', 'bar'],
ids: store.state.tags.map(t => t.name), params: routeId,
params: wrapper.vm.$route.params.id,
}); });
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items });
expect(wrapper.vm.itemsToBeDeleted).toEqual([]); it('clears the selectedItems', () => {
expect(findCheckedCheckboxes()).toHaveLength(0); findDeleteModal().vm.$emit('confirmDelete');
expect(wrapper.vm.selectedItems).toEqual([]);
}); });
}); });
}); });
});
it('tracks cancel_delete when cancel event is emitted', () => { describe('Header', () => {
const deleteModal = findDeleteModal(); it('exists', () => {
deleteModal.vm.$emit('cancel'); mountComponent();
return wrapper.vm.$nextTick().then(() => { expect(findDetailsHeader().exists()).toBe(true);
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { });
label: 'registry_tag_delete',
}); it('has the correct props', () => {
}); mountComponent();
expect(findDetailsHeader().props()).toEqual({ imageName: 'foo' });
}); });
}); });
describe('Delete alert', () => { describe('Delete Alert', () => {
const config = { const config = {
garbageCollectionHelpPagePath: 'foo', isAdmin: true,
garbageCollectionHelpPagePath: 'baz',
}; };
const deleteAlertType = 'success_tag';
describe('when the user is an admin', () => { it('exists', () => {
beforeEach(() => { mountComponent();
store.commit(SET_INITIAL_STATE, { ...config, isAdmin: true }); expect(findDeleteAlert().exists()).toBe(true);
}); });
afterEach(() => {
store.commit(SET_INITIAL_STATE, config);
});
describe.each`
deleteType | successTitle | errorTitle
${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
`('behaves correctly on $deleteType', ({ deleteType, successTitle, errorTitle }) => {
describe('when delete is successful', () => {
beforeEach(() => {
dispatchSpy.mockResolvedValue();
mountComponent();
return wrapper.vm[deleteType]('foo');
});
it('alert exists', () => {
expect(findAlert().exists()).toBe(true);
});
it('alert body contains admin tip', () => {
expect(
findAlert()
.text()
.replace(/\s\s+/gm, ' '),
).toBe(ADMIN_GARBAGE_COLLECTION_TIP.replace(/%{\w+}/gm, ''));
});
it('alert body contains link', () => {
const alertLink = findAlert().find(GlLink);
expect(alertLink.exists()).toBe(true);
expect(alertLink.attributes('href')).toBe(config.garbageCollectionHelpPagePath);
});
it('alert title is appropriate', () => {
expect(findAlert().attributes('title')).toBe(successTitle);
});
});
describe('when delete is not successful', () => {
beforeEach(() => {
mountComponent();
dispatchSpy.mockRejectedValue();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => { it('has the correct props', () => {
expect(findAlert().exists()).toBe(true); store.commit(SET_INITIAL_STATE, { ...config });
expect(findAlert().text()).toBe(errorTitle); mountComponent({
}); data: () => ({
}); deleteAlertType,
}),
}); });
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
}); });
describe.each`
deleteType | successTitle | errorTitle
${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
`(
'when the user is not an admin alert behaves correctly on $deleteType',
({ deleteType, successTitle, errorTitle }) => {
beforeEach(() => {
store.commit('SET_INITIAL_STATE', { ...config });
});
describe('when delete is successful', () => {
beforeEach(() => {
dispatchSpy.mockResolvedValue();
mountComponent();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(successTitle);
});
});
describe('when delete is not successful', () => {
beforeEach(() => {
mountComponent();
dispatchSpy.mockRejectedValue();
return wrapper.vm[deleteType]('foo');
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorTitle);
});
});
},
);
}); });
}); });
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