Commit 238160c2 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '218545-refactor-container-registry-frontend-code-to-ease-community-contribution-2' into 'master'

Split container registry details page

See merge request gitlab-org/gitlab!33829
parents bcd225d7 9645e8e0
<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';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
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 {
GlIcon,
GlTooltipDirective,
GlPagination,
GlModal,
GlSprintf,
GlAlert,
GlLink,
GlEmptyState,
GlResizeObserverDirective,
GlSkeletonLoader,
......@@ -21,6 +17,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
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 {
LIST_KEY_TAG,
......@@ -33,34 +32,29 @@ import {
LIST_LABEL_IMAGE_ID,
LIST_LABEL_SIZE,
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_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
} from '../constants/index';
export default {
components: {
DeleteAlert,
DetailsHeader,
GlTable,
GlFormCheckbox,
GlDeprecatedButton,
GlIcon,
ClipboardButton,
GlPagination,
GlModal,
DeleteModal,
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
GlAlert,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -73,18 +67,11 @@ export default {
height: 40,
},
i18n: {
DETAILS_PAGE_TITLE,
REMOVE_TAGS_BUTTON_TITLE,
REMOVE_TAG_BUTTON_TITLE,
EMPTY_IMAGE_REPOSITORY_TITLE,
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() {
return {
selectedItems: [],
......@@ -92,7 +79,7 @@ export default {
selectAllChecked: false,
modalDescription: null,
isDesktop: true,
deleteAlertType: false,
deleteAlertType: null,
};
},
computed: {
......@@ -119,21 +106,12 @@ export default {
{ key: LIST_KEY_ACTIONS, label: '' },
].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
},
isMultiDelete() {
return this.itemsToBeDeleted.length > 1;
},
tracking() {
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: {
get() {
return this.tagsPagination.page;
......@@ -142,47 +120,12 @@ export default {
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() {
this.requestTagsList({ params: this.$route.params.id });
},
methods: {
...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) {
return numberToHumanSize(size);
},
......@@ -197,53 +140,49 @@ export default {
}
},
selectAll() {
this.selectedItems = this.tags.map((x, index) => index);
this.selectedItems = this.tags.map(x => x.name);
this.selectAllChecked = true;
},
deselectAll() {
this.selectedItems = [];
this.selectAllChecked = false;
},
updateSelectedItems(index) {
const delIndex = this.selectedItems.findIndex(x => x === index);
updateSelectedItems(name) {
const delIndex = this.selectedItems.findIndex(x => x === name);
if (delIndex > -1) {
this.selectedItems.splice(delIndex, 1);
this.selectAllChecked = false;
} else {
this.selectedItems.push(index);
this.selectedItems.push(name);
if (this.selectedItems.length === this.tags.length) {
this.selectAllChecked = true;
}
}
},
deleteSingleItem(index) {
this.setModalDescription(index);
this.itemsToBeDeleted = [index];
deleteSingleItem(name) {
this.itemsToBeDeleted = [{ ...this.tags.find(t => t.name === name) }];
this.track('click_button');
this.$refs.deleteModal.show();
},
deleteMultipleItems() {
this.itemsToBeDeleted = [...this.selectedItems];
if (this.selectedItems.length === 1) {
this.setModalDescription(this.itemsToBeDeleted[0]);
} else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
this.itemsToBeDeleted = this.selectedItems.map(name => ({
...this.tags.find(t => t.name === name),
}));
this.track('click_button');
this.$refs.deleteModal.show();
},
handleSingleDelete(index) {
const itemToDelete = this.tags[index];
handleSingleDelete() {
const [itemToDelete] = 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 })
.then(() => {
this.deleteAlertType = 'success_tag';
this.deleteAlertType = ALERT_SUCCESS_TAG;
})
.catch(() => {
this.deleteAlertType = 'danger_tag';
this.deleteAlertType = ALERT_DANGER_TAG;
});
},
handleMultipleDelete() {
......@@ -252,22 +191,22 @@ export default {
this.selectedItems = [];
return this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name),
ids: itemsToBeDeleted.map(x => x.name),
params: this.$route.params.id,
})
.then(() => {
this.deleteAlertType = 'success_tags';
this.deleteAlertType = ALERT_SUCCESS_TAGS;
})
.catch(() => {
this.deleteAlertType = 'danger_tags';
this.deleteAlertType = ALERT_DANGER_TAGS;
});
},
onDeletionConfirmed() {
this.track('confirm_delete');
if (this.isMultiDelete) {
if (this.itemsToBeDeleted.length > 1) {
this.handleMultipleDelete();
} else {
this.handleSingleDelete(this.itemsToBeDeleted[0]);
this.handleSingleDelete();
}
},
handleResize() {
......@@ -279,30 +218,14 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
<gl-alert
v-if="deleteAlertType"
:variant="deleteAlertConfig.type"
:title="deleteAlertConfig.title"
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
:is-admin="config.isAdmin"
class="my-2"
@dismiss="deleteAlertType = null"
>
<gl-sprintf :message="deleteAlertConfig.message">
<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>
/>
<details-header :image-name="imageName" />
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
<template v-if="isDesktop" #head(checkbox)>
......@@ -327,12 +250,12 @@ export default {
</gl-deprecated-button>
</template>
<template #cell(checkbox)="{index}">
<template #cell(checkbox)="{item}">
<gl-form-checkbox
ref="rowCheckbox"
class="js-row-checkbox"
:checked="selectedItems.includes(index)"
@change="updateSelectedItems(index)"
:checked="selectedItems.includes(item.name)"
@change="updateSelectedItems(item.name)"
/>
</template>
<template #cell(name)="{item, field}">
......@@ -373,7 +296,7 @@ export default {
{{ timeFormatted(value) }}
</span>
</template>
<template #cell(actions)="{index, item}">
<template #cell(actions)="{item}">
<gl-deprecated-button
ref="singleDeleteButton"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
......@@ -381,7 +304,7 @@ export default {
:disabled="!item.destroy_path"
variant="danger"
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-deprecated-button>
......@@ -425,22 +348,11 @@ export default {
class="w-100"
/>
<gl-modal
<delete-modal
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
@ok="onDeletionConfirmed"
:items-to-be-deleted="itemsToBeDeleted"
@confirmDelete="onDeletionConfirmed"
@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>
</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 = {
export const tagsListResponse = {
data: [
{
tag: 'centos6',
name: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
short_revision: 'b118ab5b0',
size: 19,
......@@ -75,7 +75,7 @@ export const tagsListResponse = {
destroy_path: 'path',
},
{
tag: 'test-image',
name: 'test-tag',
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
short_revision: 'b969de599',
size: 19,
......
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 stubChildren from 'helpers/stub_children';
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 {
SET_MAIN_LOADING,
SET_INITIAL_STATE,
SET_TAGS_LIST_SUCCESS,
SET_TAGS_PAGINATION,
SET_INITIAL_STATE,
} 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 { GlModal } from '../stubs';
import { $toast } from '../../shared/mocks';
describe('Details Page', () => {
......@@ -26,7 +22,7 @@ describe('Details Page', () => {
let dispatchSpy;
let store;
const findDeleteModal = () => wrapper.find(GlModal);
const findDeleteModal = () => wrapper.find(DeleteModal);
const findPagination = () => wrapper.find(GlPagination);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
......@@ -38,7 +34,8 @@ describe('Details Page', () => {
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
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' }));
......@@ -47,9 +44,9 @@ describe('Details Page', () => {
store,
stubs: {
...stubChildren(component),
GlModal,
GlSprintf: false,
GlTable,
DeleteModal,
},
mocks: {
$route: {
......@@ -70,6 +67,7 @@ describe('Details Page', () => {
store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
jest.spyOn(Tracking, 'event');
jest.spyOn(DeleteModal.methods, 'show');
});
afterEach(() => {
......@@ -100,10 +98,6 @@ describe('Details Page', () => {
});
describe('table', () => {
beforeEach(() => {
mountComponent();
});
it.each([
'rowCheckbox',
'rowName',
......@@ -112,6 +106,7 @@ describe('Details Page', () => {
'rowTime',
'singleDeleteButton',
])('%s exist in the table', element => {
mountComponent();
expect(findFirstRowItem(element).exists()).toBe(true);
});
......@@ -143,16 +138,20 @@ describe('Details Page', () => {
});
describe('row checkbox', () => {
beforeEach(() => {
mountComponent();
});
it('if selected adds item to selectedItems', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
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();
});
});
it('if deselect remove index from selectedItems', () => {
wrapper.setData({ selectedItems: [1] });
it('if deselect remove name from selectedItems', () => {
wrapper.setData({ selectedItems: [store.state.tags[1].name] });
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectedItems.length).toBe(0);
......@@ -167,14 +166,17 @@ describe('Details Page', () => {
});
it('exists', () => {
mountComponent();
expect(findBulkDeleteButton().exists()).toBe(true);
});
it('is disabled if no item is selected', () => {
mountComponent();
expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
});
it('is enabled if at least one item is selected', () => {
mountComponent({ data: () => ({ selectedItems: [store.state.tags[0].name] }) });
wrapper.setData({ selectedItems: [1] });
return wrapper.vm.$nextTick().then(() => {
expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
......@@ -183,30 +185,26 @@ describe('Details Page', () => {
describe('on click', () => {
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');
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain(
'You are about to remove <b>foo</b>. Are you sure?',
);
expect(GlModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
expect(wrapper.vm.itemsToBeDeleted).toEqual([store.state.tags[0]]);
expect(DeleteModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
});
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');
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain(
'You are about to remove <b>2</b> tags. Are you sure?',
);
expect(GlModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'bulk_registry_tag_delete',
});
expect(wrapper.vm.itemsToBeDeleted).toEqual(tagsListResponse.data);
expect(DeleteModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'bulk_registry_tag_delete',
});
});
});
......@@ -237,14 +235,10 @@ describe('Details Page', () => {
findAllDeleteButtons()
.at(0)
.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain(
'You are about to remove <b>bar</b>. Are you sure?',
);
expect(GlModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
expect(DeleteModal.methods.show).toHaveBeenCalled();
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete',
});
});
});
......@@ -292,6 +286,7 @@ describe('Details Page', () => {
let timeCell;
beforeEach(() => {
mountComponent();
timeCell = findFirstRowItem('rowTime');
});
......@@ -331,176 +326,97 @@ describe('Details Page', () => {
});
describe('modal', () => {
beforeEach(() => {
mountComponent();
});
it('exists', () => {
mountComponent();
expect(findDeleteModal().exists()).toBe(true);
});
describe('when ok event is emitted', () => {
beforeEach(() => {
dispatchSpy.mockResolvedValue();
});
it('tracks confirm_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('cancel event', () => {
it('tracks cancel_delete', () => {
mountComponent();
findDeleteModal().vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
label: 'registry_tag_delete',
});
});
});
describe('when only one element is selected', () => {
it('execute the delete and remove selection', () => {
wrapper.setData({ itemsToBeDeleted: [0] });
findDeleteModal().vm.$emit('ok');
describe('confirmDelete event', () => {
describe('when one item is selected to be deleted', () => {
const itemsToBeDeleted = [{ name: 'foo' }];
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0],
params: wrapper.vm.$route.params.id,
it('dispatch requestDeleteTag with the right parameters', () => {
mountComponent({ data: () => ({ itemsToBeDeleted }) });
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([]);
expect(wrapper.vm.selectedItems).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0);
});
it('remove the deleted item from the selected items', () => {
mountComponent({ data: () => ({ itemsToBeDeleted, selectedItems: ['foo', 'bar'] }) });
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(() => {
wrapper.setData({ itemsToBeDeleted: [0, 1] });
mountComponent({
data: () => ({
itemsToBeDeleted: [{ name: 'foo' }, { name: 'bar' }],
selectedItems: ['foo', 'bar'],
}),
});
});
it('execute the delete and remove selection', () => {
findDeleteModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name),
params: wrapper.vm.$route.params.id,
it('dispatch requestDeleteTags with the right parameters', () => {
findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
ids: ['foo', 'bar'],
params: routeId,
});
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0);
});
it('clears the selectedItems', () => {
findDeleteModal().vm.$emit('confirmDelete');
expect(wrapper.vm.selectedItems).toEqual([]);
});
});
});
});
it('tracks cancel_delete when cancel event is emitted', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
return wrapper.vm.$nextTick().then(() => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
label: 'registry_tag_delete',
});
});
describe('Header', () => {
it('exists', () => {
mountComponent();
expect(findDetailsHeader().exists()).toBe(true);
});
it('has the correct props', () => {
mountComponent();
expect(findDetailsHeader().props()).toEqual({ imageName: 'foo' });
});
});
describe('Delete alert', () => {
describe('Delete Alert', () => {
const config = {
garbageCollectionHelpPagePath: 'foo',
isAdmin: true,
garbageCollectionHelpPagePath: 'baz',
};
const deleteAlertType = 'success_tag';
describe('when the user is an admin', () => {
beforeEach(() => {
store.commit(SET_INITIAL_STATE, { ...config, isAdmin: 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('exists', () => {
mountComponent();
expect(findDeleteAlert().exists()).toBe(true);
});
it('alert exist and text is appropriate', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorTitle);
});
});
it('has the correct props', () => {
store.commit(SET_INITIAL_STATE, { ...config });
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