Commit 2361e6ae authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Bob Van Landuyt

Add gid prefix utility to views

- utility
- project
- group
parent 4ed1a755
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
return this.tags.some(tag => this.selectedItems[tag.name]); return this.tags.some(tag => this.selectedItems[tag.name]);
}, },
showMultiDeleteButton() { showMultiDeleteButton() {
return this.tags.some(tag => tag.destroy_path) && !this.isMobile; return this.tags.some(tag => tag.canDelete) && !this.isMobile;
}, },
}, },
methods: { methods: {
......
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
}, },
computed: { computed: {
formattedSize() { formattedSize() {
return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE; return this.tag.totalSize ? numberToHumanSize(this.tag.totalSize) : NOT_AVAILABLE_SIZE;
}, },
layers() { layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : ''; return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
...@@ -76,10 +76,10 @@ export default { ...@@ -76,10 +76,10 @@ export default {
return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT; return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT;
}, },
publishedDate() { publishedDate() {
return formatDate(this.tag.created_at, 'isoDate'); return formatDate(this.tag.createdAt, 'isoDate');
}, },
publishedTime() { publishedTime() {
return formatDate(this.tag.created_at, 'hh:MM Z'); return formatDate(this.tag.createdAt, 'hh:MM Z');
}, },
formattedRevision() { formattedRevision() {
// to be removed when API response is adjusted // to be removed when API response is adjusted
...@@ -101,7 +101,7 @@ export default { ...@@ -101,7 +101,7 @@ export default {
<list-item v-bind="$attrs" :selected="selected"> <list-item v-bind="$attrs" :selected="selected">
<template #left-action> <template #left-action>
<gl-form-checkbox <gl-form-checkbox
v-if="Boolean(tag.destroy_path)" v-if="tag.canDelete"
:disabled="invalidTag" :disabled="invalidTag"
class="gl-m-0" class="gl-m-0"
:checked="selected" :checked="selected"
...@@ -148,7 +148,7 @@ export default { ...@@ -148,7 +148,7 @@ export default {
<span data-testid="time"> <span data-testid="time">
<gl-sprintf :message="$options.i18n.CREATED_AT_LABEL"> <gl-sprintf :message="$options.i18n.CREATED_AT_LABEL">
<template #timeInfo> <template #timeInfo>
<time-ago-tooltip :time="tag.created_at" /> <time-ago-tooltip :time="tag.createdAt" />
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </span>
...@@ -162,10 +162,10 @@ export default { ...@@ -162,10 +162,10 @@ export default {
</template> </template>
<template #right-action> <template #right-action>
<delete-button <delete-button
:disabled="!tag.destroy_path || invalidTag" :disabled="!tag.canDelete || invalidTag"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP" :tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="Boolean(tag.destroy_path)" :tooltip-disabled="tag.canDelete"
data-testid="single-delete-button" data-testid="single-delete-button"
@delete="$emit('delete')" @delete="$emit('delete')"
/> />
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
// We are forced to use `v-html` untill this gitlab-ui MR is merged: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1869 // We are forced to use `v-html` untill this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
// then we can re-write this to use gl-breadcrumb // then we can re-write this to use gl-breadcrumb
import { initial, first, last } from 'lodash'; import { initial, first, last } from 'lodash';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
return { return {
tagName, tagName,
className, className,
text: this.$route.meta.nameGenerator(this.$store.state), text: this.$route.meta.nameGenerator(),
path: { to: this.$route.name }, path: { to: this.$route.name },
}; };
}, },
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
></li> ></li>
<li v-if="!isRootRoute"> <li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path"> <router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator($store.state) }} {{ rootRoute.meta.nameGenerator() }}
</router-link> </router-link>
<component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" /> <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
</li> </li>
......
mutation destroyContainerRepositoryTags($id: ContainerRepositoryID!, $tagNames: [String!]!) {
destroyContainerRepositoryTags(input: { id: $id, tagNames: $tagNames }) {
errors
}
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getContainerRepositoryDetails(
$id: ID!
$first: Int
$last: Int
$after: String
$before: String
) {
containerRepository(id: $id) {
id
name
path
status
location
canDelete
createdAt
tagsCount
expirationPolicyStartedAt
tags(after: $after, before: $before, first: $first, last: $last) {
nodes {
digest
location
path
name
revision
shortRevision
createdAt
totalSize
canDelete
}
pageInfo {
...PageInfo
}
}
}
}
...@@ -19,8 +19,16 @@ export default () => { ...@@ -19,8 +19,16 @@ export default () => {
const { endpoint } = el.dataset; const { endpoint } = el.dataset;
// This is a mini state to help the breadcrumb have the correct name in the details page
const breadCrumbState = Vue.observable({
name: '',
updateName(value) {
this.name = value;
},
});
const store = createStore(); const store = createStore();
const router = createRouter(endpoint); const router = createRouter(endpoint, breadCrumbState);
store.dispatch('setInitialState', el.dataset); store.dispatch('setInitialState', el.dataset);
const attachMainComponent = () => const attachMainComponent = () =>
...@@ -32,6 +40,9 @@ export default () => { ...@@ -32,6 +40,9 @@ export default () => {
components: { components: {
RegistryExplorer, RegistryExplorer,
}, },
provide() {
return { breadCrumbState };
},
render(createElement) { render(createElement) {
return createElement('registry-explorer'); return createElement('registry-explorer');
}, },
...@@ -42,8 +53,8 @@ export default () => { ...@@ -42,8 +53,8 @@ export default () => {
const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')]; const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')];
return new Vue({ return new Vue({
el: breadCrumbEl, el: breadCrumbEl,
store,
router, router,
apolloProvider,
components: { components: {
RegistryBreadcrumb, RegistryBreadcrumb,
}, },
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState } from 'vuex';
import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui'; import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { joinPaths } from '~/lib/utils/url_utility';
import DeleteAlert from '../components/details_page/delete_alert.vue'; import DeleteAlert from '../components/details_page/delete_alert.vue';
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue'; import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue'; import DeleteModal from '../components/details_page/delete_modal.vue';
...@@ -11,11 +13,16 @@ import TagsList from '../components/details_page/tags_list.vue'; ...@@ -11,11 +13,16 @@ import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue'; import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue'; import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.graphql';
import { import {
ALERT_SUCCESS_TAG, ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG, ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS, ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS, ALERT_DANGER_TAGS,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index'; } from '../constants/index';
export default { export default {
...@@ -23,28 +30,62 @@ export default { ...@@ -23,28 +30,62 @@ export default {
DeleteAlert, DeleteAlert,
PartialCleanupAlert, PartialCleanupAlert,
DetailsHeader, DetailsHeader,
GlPagination, GlKeysetPagination,
DeleteModal, DeleteModal,
TagsList, TagsList,
TagsLoader, TagsLoader,
EmptyTagsState, EmptyTagsState,
}, },
inject: ['breadCrumbState'],
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
apollo: {
image: {
query: getContainerRepositoryDetailsQuery,
variables() {
return this.queryVariables;
},
update(data) {
return data.containerRepository;
},
result({ data }) {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
this.breadCrumbState.updateName(data.containerRepository?.name);
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
data() { data() {
return { return {
image: {},
tagsPageInfo: {},
itemsToBeDeleted: [], itemsToBeDeleted: [],
isMobile: false, isMobile: false,
mutationLoading: false,
deleteAlertType: null, deleteAlertType: null,
dismissPartialCleanupWarning: false, dismissPartialCleanupWarning: false,
}; };
}, },
computed: { computed: {
...mapState(['tagsPagination', 'isLoading', 'config', 'tags', 'imageDetails']), ...mapState(['config']),
queryVariables() {
return {
id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`),
first: GRAPHQL_PAGE_SIZE,
};
},
isLoading() {
return this.$apollo.queries.image.loading || this.mutationLoading;
},
tags() {
return this.image?.tags?.nodes || [];
},
showPartialCleanupWarning() { showPartialCleanupWarning() {
return this.imageDetails?.cleanup_policy_started_at && !this.dismissPartialCleanupWarning; return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning;
}, },
tracking() { tracking() {
return { return {
...@@ -52,66 +93,78 @@ export default { ...@@ -52,66 +93,78 @@ export default {
this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
}; };
}, },
currentPage: { showPagination() {
get() { return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
return this.tagsPagination.page;
},
set(page) {
this.requestTagsList({ page });
},
}, },
}, },
mounted() {
this.requestImageDetailsAndTagsList(this.$route.params.id);
},
methods: { methods: {
...mapActions([
'requestTagsList',
'requestDeleteTag',
'requestDeleteTags',
'requestImageDetailsAndTagsList',
]),
deleteTags(toBeDeleted) { deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]); this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]);
this.track('click_button'); this.track('click_button');
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
handleSingleDelete() { async handleDelete() {
const [itemToDelete] = this.itemsToBeDeleted; this.track('confirm_delete');
this.itemsToBeDeleted = [];
return this.requestDeleteTag({ tag: itemToDelete })
.then(() => {
this.deleteAlertType = ALERT_SUCCESS_TAG;
})
.catch(() => {
this.deleteAlertType = ALERT_DANGER_TAG;
});
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this; const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = []; this.itemsToBeDeleted = [];
this.mutationLoading = true;
return this.requestDeleteTags({ try {
ids: itemsToBeDeleted.map(x => x.name), const { data } = await this.$apollo.mutate({
}) mutation: deleteContainerRepositoryTagsMutation,
.then(() => { variables: {
this.deleteAlertType = ALERT_SUCCESS_TAGS; id: this.queryVariables.id,
}) tagNames: itemsToBeDeleted.map(i => i.name),
.catch(() => { },
this.deleteAlertType = ALERT_DANGER_TAGS; awaitRefetchQueries: true,
refetchQueries: [
{
query: getContainerRepositoryDetailsQuery,
variables: this.queryVariables,
},
],
}); });
},
onDeletionConfirmed() { if (data?.destroyContainerRepositoryTags?.errors[0]) {
this.track('confirm_delete'); throw new Error();
if (this.itemsToBeDeleted.length > 1) { }
this.handleMultipleDelete(); this.deleteAlertType =
} else { itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS;
this.handleSingleDelete(); } catch (e) {
this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
} }
this.mutationLoading = false;
}, },
handleResize() { handleResize() {
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs'; this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
}, },
fetchNextPage() {
if (this.tagsPageInfo?.hasNextPage) {
this.$apollo.queries.image.fetchMore({
variables: {
after: this.tagsPageInfo?.endCursor,
first: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
fetchPreviousPage() {
if (this.tagsPageInfo?.hasPreviousPage) {
this.$apollo.queries.image.fetchMore({
variables: {
first: null,
before: this.tagsPageInfo?.startCursor,
last: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
}, },
}; };
</script> </script>
...@@ -132,28 +185,30 @@ export default { ...@@ -132,28 +185,30 @@ export default {
@dismiss="dismissPartialCleanupWarning = true" @dismiss="dismissPartialCleanupWarning = true"
/> />
<details-header :image-name="imageDetails.name" /> <details-header :image-name="image.name" />
<tags-loader v-if="isLoading" /> <tags-loader v-if="isLoading" />
<template v-else> <template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" /> <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
<tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" /> <template v-else>
<tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
:has-next-page="tagsPageInfo.hasNextPage"
:has-previous-page="tagsPageInfo.hasPreviousPage"
class="gl-mt-3"
@prev="fetchPreviousPage"
@next="fetchNextPage"
/>
</div>
</template>
</template> </template>
<gl-pagination
v-if="!isLoading"
ref="pagination"
v-model="currentPage"
:per-page="tagsPagination.perPage"
:total-items="tagsPagination.total"
align="center"
class="gl-w-full gl-mt-3"
/>
<delete-modal <delete-modal
ref="deleteModal" ref="deleteModal"
:items-to-be-deleted="itemsToBeDeleted" :items-to-be-deleted="itemsToBeDeleted"
@confirmDelete="onDeletionConfirmed" @confirmDelete="handleDelete"
@cancel="track('cancel_delete')" @cancel="track('cancel_delete')"
/> />
</div> </div>
......
...@@ -6,7 +6,7 @@ import { CONTAINER_REGISTRY_TITLE } from './constants/index'; ...@@ -6,7 +6,7 @@ import { CONTAINER_REGISTRY_TITLE } from './constants/index';
Vue.use(VueRouter); Vue.use(VueRouter);
export default function createRouter(base) { export default function createRouter(base, breadCrumbState) {
const router = new VueRouter({ const router = new VueRouter({
base, base,
mode: 'history', mode: 'history',
...@@ -25,7 +25,7 @@ export default function createRouter(base) { ...@@ -25,7 +25,7 @@ export default function createRouter(base) {
path: '/:id', path: '/:id',
component: Details, component: Details,
meta: { meta: {
nameGenerator: ({ imageDetails }) => imageDetails?.name, nameGenerator: () => breadCrumbState.name,
}, },
}, },
], ],
......
...@@ -5,4 +5,8 @@ module ContainerRegistryHelper ...@@ -5,4 +5,8 @@ module ContainerRegistryHelper
Feature.enabled?(:container_registry_expiration_policies_throttling) && Feature.enabled?(:container_registry_expiration_policies_throttling) &&
ContainerRegistry::Client.supports_tag_delete? ContainerRegistry::Client.supports_tag_delete?
end end
def container_repository_gid_prefix
"gid://#{GlobalID.app}/#{ContainerRepository.name}/"
end
end end
...@@ -17,4 +17,5 @@ ...@@ -17,4 +17,5 @@
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
is_group_page: "true", is_group_page: "true",
"group_path": @group.full_path, "group_path": @group.full_path,
"gid_prefix": container_repository_gid_prefix,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
...@@ -18,5 +18,6 @@ ...@@ -18,5 +18,6 @@
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path, "project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
...@@ -94,7 +94,7 @@ RSpec.describe 'Container Registry', :js do ...@@ -94,7 +94,7 @@ RSpec.describe 'Container Registry', :js do
end end
it('pagination navigate to the second page') do it('pagination navigate to the second page') do
visit_details_second_page visit_next_page
expect(page).to have_content '20' expect(page).to have_content '20'
end end
...@@ -128,12 +128,12 @@ RSpec.describe 'Container Registry', :js do ...@@ -128,12 +128,12 @@ RSpec.describe 'Container Registry', :js do
end end
it 'pagination goes to second page' do it 'pagination goes to second page' do
visit_list_next_page visit_next_page
expect(page).to have_content 'my/image' expect(page).to have_content 'my/image'
end end
it 'pagination is preserved after navigating back from details' do it 'pagination is preserved after navigating back from details' do
visit_list_next_page visit_next_page
click_link 'my/image' click_link 'my/image'
breadcrumb = find '.breadcrumbs' breadcrumb = find '.breadcrumbs'
breadcrumb.click_link 'Container Registry' breadcrumb.click_link 'Container Registry'
...@@ -150,13 +150,8 @@ RSpec.describe 'Container Registry', :js do ...@@ -150,13 +150,8 @@ RSpec.describe 'Container Registry', :js do
click_link name click_link name
end end
def visit_list_next_page def visit_next_page
pagination = find '.gl-keyset-pagination' pagination = find '.gl-keyset-pagination'
pagination.click_button 'Next' pagination.click_button 'Next'
end end
def visit_details_second_page
pagination = find '.gl-pagination'
pagination.click_link '2'
end
end end
...@@ -20,7 +20,7 @@ import { ListItem } from '../../stubs'; ...@@ -20,7 +20,7 @@ import { ListItem } from '../../stubs';
describe('tags list row', () => { describe('tags list row', () => {
let wrapper; let wrapper;
const [tag] = [...tagsListResponse.data]; const [tag] = [...tagsListResponse];
const defaultProps = { tag, isMobile: false, index: 0 }; const defaultProps = { tag, isMobile: false, index: 0 };
...@@ -65,7 +65,7 @@ describe('tags list row', () => { ...@@ -65,7 +65,7 @@ describe('tags list row', () => {
}); });
it("does not exist when the row can't be deleted", () => { it("does not exist when the row can't be deleted", () => {
const customTag = { ...tag, destroy_path: '' }; const customTag = { ...tag, canDelete: false };
mountComponent({ ...defaultProps, tag: customTag }); mountComponent({ ...defaultProps, tag: customTag });
...@@ -137,8 +137,8 @@ describe('tags list row', () => { ...@@ -137,8 +137,8 @@ describe('tags list row', () => {
mountComponent(); mountComponent();
expect(findClipboardButton().attributes()).toMatchObject({ expect(findClipboardButton().attributes()).toMatchObject({
text: 'location', text: tag.location,
title: 'location', title: tag.location,
}); });
}); });
}); });
...@@ -171,26 +171,26 @@ describe('tags list row', () => { ...@@ -171,26 +171,26 @@ describe('tags list row', () => {
expect(findSize().exists()).toBe(true); expect(findSize().exists()).toBe(true);
}); });
it('contains the total_size and layers', () => { it('contains the totalSize and layers', () => {
mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024 } }); mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 1024 } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB · 10 layers'); expect(findSize().text()).toMatchInterpolatedText('1.00 KiB · 10 layers');
}); });
it('when total_size is missing', () => { it('when totalSize is missing', () => {
mountComponent(); mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 0 } });
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`); expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`);
}); });
it('when layers are missing', () => { it('when layers are missing', () => {
mountComponent({ ...defaultProps, tag: { ...tag, total_size: 1024, layers: null } }); mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 1024, layers: null } });
expect(findSize().text()).toMatchInterpolatedText('1.00 KiB'); expect(findSize().text()).toMatchInterpolatedText('1.00 KiB');
}); });
it('when there is 1 layer', () => { it('when there is 1 layer', () => {
mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } }); mountComponent({ ...defaultProps, tag: { ...tag, totalSize: 0, layers: 1 } });
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`); expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`);
}); });
...@@ -218,7 +218,7 @@ describe('tags list row', () => { ...@@ -218,7 +218,7 @@ describe('tags list row', () => {
it('pass the correct props to time ago tooltip', () => { it('pass the correct props to time ago tooltip', () => {
mountComponent(); mountComponent();
expect(findTimeAgoTooltip().attributes()).toMatchObject({ time: tag.created_at }); expect(findTimeAgoTooltip().attributes()).toMatchObject({ time: tag.createdAt });
}); });
}); });
...@@ -232,7 +232,7 @@ describe('tags list row', () => { ...@@ -232,7 +232,7 @@ describe('tags list row', () => {
it('has the correct text', () => { it('has the correct text', () => {
mountComponent(); mountComponent();
expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5'); expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 9d72ae1');
}); });
it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => { it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => {
...@@ -260,18 +260,15 @@ describe('tags list row', () => { ...@@ -260,18 +260,15 @@ describe('tags list row', () => {
}); });
it.each` it.each`
destroy_path | digest canDelete | digest
${'foo'} | ${null} ${true} | ${null}
${null} | ${'foo'} ${false} | ${'foo'}
${null} | ${null} ${false} | ${null}
`( `('is disabled when canDelete is $canDelete and digest is $digest', ({ canDelete, digest }) => {
'is disabled when destroy_path is $destroy_path and digest is $digest', mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest } });
({ destroy_path, digest }) => {
mountComponent({ ...defaultProps, tag: { ...tag, destroy_path, digest } }); expect(findDeleteButton().attributes('disabled')).toBe('true');
});
expect(findDeleteButton().attributes('disabled')).toBe('true');
},
);
it('delete event emits delete', () => { it('delete event emits delete', () => {
mountComponent(); mountComponent();
...@@ -295,10 +292,10 @@ describe('tags list row', () => { ...@@ -295,10 +292,10 @@ describe('tags list row', () => {
}); });
describe.each` describe.each`
name | finderFunction | text | icon | clipboard name | finderFunction | text | icon | clipboard
${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false} ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 01:29 GMT+0000 on 2020-11-03'} | ${'clock'} | ${false}
${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true} ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:9d72ae1db47404e44e1760eb1ca4cb427b84be8c511f05dfe2089e1b9f741dd7'} | ${'log'} | ${true}
${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true} ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:5183b5d133fa864dca2de602f874b0d1bffe0f204ad894e3660432a487935139'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => { `('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, () => { it(`has ${text} as text`, () => {
expect(finderFunction().text()).toMatchInterpolatedText(text); expect(finderFunction().text()).toMatchInterpolatedText(text);
......
...@@ -7,8 +7,8 @@ import { tagsListResponse } from '../../mock_data'; ...@@ -7,8 +7,8 @@ import { tagsListResponse } from '../../mock_data';
describe('Tags List', () => { describe('Tags List', () => {
let wrapper; let wrapper;
const tags = [...tagsListResponse.data]; const tags = [...tagsListResponse];
const readOnlyTags = tags.map(t => ({ ...t, destroy_path: undefined })); const readOnlyTags = tags.map(t => ({ ...t, canDelete: false }));
const findTagsListRow = () => wrapper.findAll(TagsListRow); const findTagsListRow = () => wrapper.findAll(TagsListRow);
const findDeleteButton = () => wrapper.find(GlButton); const findDeleteButton = () => wrapper.find(GlButton);
...@@ -92,7 +92,7 @@ describe('Tags List', () => { ...@@ -92,7 +92,7 @@ describe('Tags List', () => {
.vm.$emit('select'); .vm.$emit('select');
findDeleteButton().vm.$emit('click'); findDeleteButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]); expect(wrapper.emitted('delete')).toEqual([[{ 'alpha-11821': true }]]);
}); });
}); });
...@@ -132,7 +132,7 @@ describe('Tags List', () => { ...@@ -132,7 +132,7 @@ describe('Tags List', () => {
findTagsListRow() findTagsListRow()
.at(0) .at(0)
.vm.$emit('delete'); .vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[{ centos6: true }]]); expect(wrapper.emitted('delete')).toEqual([[{ 'alpha-11821': true }]]);
}); });
}); });
}); });
......
...@@ -32,10 +32,6 @@ describe('Registry Breadcrumb', () => { ...@@ -32,10 +32,6 @@ describe('Registry Breadcrumb', () => {
{ name: 'baz', meta: { nameGenerator } }, { name: 'baz', meta: { nameGenerator } },
]; ];
const state = {
imageDetails: { foo: 'bar' },
};
const findDivider = () => wrapper.find('.js-divider'); const findDivider = () => wrapper.find('.js-divider');
const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' }); const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' });
const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' }); const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' });
...@@ -56,9 +52,6 @@ describe('Registry Breadcrumb', () => { ...@@ -56,9 +52,6 @@ describe('Registry Breadcrumb', () => {
routes, routes,
}, },
}, },
$store: {
state,
},
}, },
}); });
}; };
...@@ -87,7 +80,6 @@ describe('Registry Breadcrumb', () => { ...@@ -87,7 +80,6 @@ describe('Registry Breadcrumb', () => {
}); });
it('the link text is calculated by nameGenerator', () => { it('the link text is calculated by nameGenerator', () => {
expect(nameGenerator).toHaveBeenCalledWith(state);
expect(nameGenerator).toHaveBeenCalledTimes(1); expect(nameGenerator).toHaveBeenCalledTimes(1);
}); });
}); });
...@@ -111,7 +103,6 @@ describe('Registry Breadcrumb', () => { ...@@ -111,7 +103,6 @@ describe('Registry Breadcrumb', () => {
}); });
it('the link text is calculated by nameGenerator', () => { it('the link text is calculated by nameGenerator', () => {
expect(nameGenerator).toHaveBeenCalledWith(state);
expect(nameGenerator).toHaveBeenCalledTimes(2); expect(nameGenerator).toHaveBeenCalledTimes(2);
}); });
}); });
......
...@@ -72,34 +72,34 @@ export const imagesListResponse = [ ...@@ -72,34 +72,34 @@ export const imagesListResponse = [
}, },
]; ];
export const tagsListResponse = { export const tagsListResponse = [
data: [ {
{ canDelete: true,
name: 'centos6', createdAt: '2020-11-03T13:29:49+00:00',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', digest: 'sha256:9d72ae1db47404e44e1760eb1ca4cb427b84be8c511f05dfe2089e1b9f741dd7',
short_revision: 'b118ab5b0', location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:alpha-11821',
size: 19, name: 'alpha-11821',
layers: 10, path: 'gitlab-org/gitlab-test/rails-12009:alpha-11821',
location: 'location', revision: '5183b5d133fa864dca2de602f874b0d1bffe0f204ad894e3660432a487935139',
path: 'bar:centos6', shortRevision: '5183b5d13',
created_at: '2020-06-29T10:23:51.766+00:00', totalSize: 104,
destroy_path: 'path', layers: 10,
digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c', __typename: 'ContainerRepositoryTag',
}, },
{ {
name: 'test-tag', canDelete: true,
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4', createdAt: '2020-11-03T13:29:48+00:00',
short_revision: 'b969de599', digest: 'sha256:64f61282a71659f72066f9decd30b9038a465859b277a5e20da8681eb83e72f7',
size: 19, location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:alpha-20825',
layers: 10, name: 'alpha-20825',
path: 'foo:test-tag', path: 'gitlab-org/gitlab-test/rails-12009:alpha-20825',
location: 'location-2', revision: 'e4212f1b73c6f9def2c37fa7df6c8d35c345fb1402860ff9a56404821aacf16f',
created_at: '2020-06-29T10:23:51.766+00:00', shortRevision: 'e4212f1b7',
digest: 'sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7736dfd5c', totalSize: 105,
}, layers: 10,
], __typename: 'ContainerRepositoryTag',
headers, },
}; ];
export const pageInfo = { export const pageInfo = {
hasNextPage: true, hasNextPage: true,
...@@ -110,14 +110,14 @@ export const pageInfo = { ...@@ -110,14 +110,14 @@ export const pageInfo = {
}; };
export const imageDetailsMock = { export const imageDetailsMock = {
id: 1, canDelete: true,
name: 'rails-32309', createdAt: '2020-11-03T13:29:21Z',
path: 'gitlab-org/gitlab-test/rails-32309', expirationPolicyStartedAt: null,
project_id: 1, id: 'gid://gitlab/ContainerRepository/26',
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-32309', location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
created_at: '2020-06-29T10:23:47.838Z', name: 'rails-12009',
cleanup_policy_started_at: null, path: 'gitlab-org/gitlab-test/rails-12009',
delete_api_path: 'http://0.0.0.0:3000/api/v4/projects/1/registry/repositories/1', status: null,
}; };
export const graphQLImageListMock = { export const graphQLImageListMock = {
...@@ -192,3 +192,96 @@ export const graphQLImageDeleteMockError = { ...@@ -192,3 +192,96 @@ export const graphQLImageDeleteMockError = {
}, },
}, },
}; };
export const containerRepositoryMock = {
id: 'gid://gitlab/ContainerRepository/26',
name: 'rails-12009',
path: 'gitlab-org/gitlab-test/rails-12009',
status: null,
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
tagsCount: 13,
expirationPolicyStartedAt: null,
};
export const tagsPageInfo = {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'MQ',
endCursor: 'MTA',
};
export const tagsMock = [
{
digest: 'sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062',
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:beta-24753',
path: 'gitlab-org/gitlab-test/rails-12009:beta-24753',
name: 'beta-24753',
revision: 'c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b',
shortRevision: 'c2613843a',
createdAt: '2020-11-03T13:29:38+00:00',
totalSize: 105,
canDelete: true,
__typename: 'ContainerRepositoryTag',
},
{
digest: 'sha256:7f94f97dff89ffd122cafe50cd32329adf682356a7a96f69cbfe313ee589791c',
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009:beta-31075',
path: 'gitlab-org/gitlab-test/rails-12009:beta-31075',
name: 'beta-31075',
revision: 'df44e7228f0f255c73e35b6f0699624a615f42746e3e8e2e4b3804a6d6fc3292',
shortRevision: 'df44e7228',
createdAt: '2020-11-03T13:29:32+00:00',
totalSize: 104,
canDelete: true,
__typename: 'ContainerRepositoryTag',
},
];
export const graphQLImageDetailsMock = override => ({
data: {
containerRepository: {
...containerRepositoryMock,
tags: {
nodes: tagsMock,
pageInfo: { ...tagsPageInfo },
__typename: 'ContainerRepositoryTagConnection',
},
__typename: 'ContainerRepositoryDetails',
...override,
},
},
});
export const graphQLImageDetailsEmptyTagsMock = {
data: {
containerRepository: {
...containerRepositoryMock,
tags: {
nodes: [],
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
__typename: 'ContainerRepositoryTagConnection',
},
__typename: 'ContainerRepositoryDetails',
},
},
};
export const graphQLDeleteImageRepositoryTagsMock = {
data: {
destroyContainerRepositoryTags: {
deletedTagNames: [],
errors: [],
__typename: 'DestroyContainerRepositoryTagsPayload',
},
},
};
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui'; import { GlKeysetPagination } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
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 DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
...@@ -9,24 +12,29 @@ import TagsLoader from '~/registry/explorer/components/details_page/tags_loader. ...@@ -9,24 +12,29 @@ import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue'; import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue'; import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
import { createStore } from '~/registry/explorer/stores/'; import { createStore } from '~/registry/explorer/stores/';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.graphql';
import { import {
SET_MAIN_LOADING, graphQLImageDetailsMock,
SET_TAGS_LIST_SUCCESS, graphQLImageDetailsEmptyTagsMock,
SET_TAGS_PAGINATION, graphQLDeleteImageRepositoryTagsMock,
SET_INITIAL_STATE, containerRepositoryMock,
SET_IMAGE_DETAILS, tagsMock,
} from '~/registry/explorer/stores/mutation_types'; tagsPageInfo,
} from '../mock_data';
import { tagsListResponse, imageDetailsMock } from '../mock_data';
import { DeleteModal } from '../stubs'; import { DeleteModal } from '../stubs';
const localVue = createLocalVue();
describe('Details Page', () => { describe('Details Page', () => {
let wrapper; let wrapper;
let dispatchSpy;
let store; let store;
let apolloProvider;
const findDeleteModal = () => wrapper.find(DeleteModal); const findDeleteModal = () => wrapper.find(DeleteModal);
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlKeysetPagination);
const findTagsLoader = () => wrapper.find(TagsLoader); const findTagsLoader = () => wrapper.find(TagsLoader);
const findTagsList = () => wrapper.find(TagsList); const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert); const findDeleteAlert = () => wrapper.find(DeleteAlert);
...@@ -36,15 +44,46 @@ describe('Details Page', () => { ...@@ -36,15 +44,46 @@ describe('Details Page', () => {
const routeId = 1; const routeId = 1;
const breadCrumbState = {
updateName: jest.fn(),
};
const cleanTags = tagsMock.map(t => {
const result = { ...t };
// eslint-disable-next-line no-underscore-dangle
delete result.__typename;
return result;
});
const waitForApolloRequestRender = async () => {
await waitForPromises();
await wrapper.vm.$nextTick();
};
const tagsArrayToSelectedTags = tags => const tagsArrayToSelectedTags = tags =>
tags.reduce((acc, c) => { tags.reduce((acc, c) => {
acc[c.name] = true; acc[c.name] = true;
return acc; return acc;
}, {}); }, {});
const mountComponent = ({ options } = {}) => { const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
options,
} = {}) => {
localVue.use(VueApollo);
const requestHandlers = [
[getContainerRepositoryDetailsQuery, resolver],
[deleteContainerRepositoryTagsMutation, mutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
store, store,
localVue,
apolloProvider,
stubs: { stubs: {
DeleteModal, DeleteModal,
}, },
...@@ -55,17 +94,17 @@ describe('Details Page', () => { ...@@ -55,17 +94,17 @@ describe('Details Page', () => {
}, },
}, },
}, },
provide() {
return {
breadCrumbState,
};
},
...options, ...options,
}); });
}; };
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
dispatchSpy.mockResolvedValue();
store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
store.commit(SET_IMAGE_DETAILS, imageDetailsMock);
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
}); });
...@@ -74,85 +113,90 @@ describe('Details Page', () => { ...@@ -74,85 +113,90 @@ describe('Details Page', () => {
wrapper = null; wrapper = null;
}); });
describe('lifecycle events', () => {
it('calls the appropriate action on mount', () => {
mountComponent();
expect(dispatchSpy).toHaveBeenCalledWith('requestImageDetailsAndTagsList', routeId);
});
});
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
beforeEach(() => { it('shows the loader', () => {
store.commit(SET_MAIN_LOADING, true);
mountComponent(); mountComponent();
});
afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('shows the loader', () => {
expect(findTagsLoader().exists()).toBe(true); expect(findTagsLoader().exists()).toBe(true);
}); });
it('does not show the list', () => { it('does not show the list', () => {
mountComponent();
expect(findTagsList().exists()).toBe(false); expect(findTagsList().exists()).toBe(false);
}); });
it('does not show pagination', () => { it('does not show pagination', () => {
mountComponent();
expect(findPagination().exists()).toBe(false); expect(findPagination().exists()).toBe(false);
}); });
}); });
describe('when the list of tags is empty', () => { describe('when the list of tags is empty', () => {
beforeEach(() => { const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
store.commit(SET_TAGS_LIST_SUCCESS, []);
mountComponent(); it('has the empty state', async () => {
}); mountComponent({ resolver });
await waitForApolloRequestRender();
it('has the empty state', () => {
expect(findEmptyTagsState().exists()).toBe(true); expect(findEmptyTagsState().exists()).toBe(true);
}); });
it('does not show the loader', () => { it('does not show the loader', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findTagsLoader().exists()).toBe(false); expect(findTagsLoader().exists()).toBe(false);
}); });
it('does not show the list', () => { it('does not show the list', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findTagsList().exists()).toBe(false); expect(findTagsList().exists()).toBe(false);
}); });
}); });
describe('list', () => { describe('list', () => {
beforeEach(() => { it('exists', async () => {
mountComponent(); mountComponent();
});
it('exists', () => { await waitForApolloRequestRender();
expect(findTagsList().exists()).toBe(true); expect(findTagsList().exists()).toBe(true);
}); });
it('has the correct props bound', () => { it('has the correct props bound', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findTagsList().props()).toMatchObject({ expect(findTagsList().props()).toMatchObject({
isMobile: false, isMobile: false,
tags: store.state.tags, tags: cleanTags,
}); });
}); });
describe('deleteEvent', () => { describe('deleteEvent', () => {
describe('single item', () => { describe('single item', () => {
let tagToBeDeleted; let tagToBeDeleted;
beforeEach(() => { beforeEach(async () => {
[tagToBeDeleted] = store.state.tags; mountComponent();
await waitForApolloRequestRender();
[tagToBeDeleted] = cleanTags;
findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true }); findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true });
}); });
it('open the modal', () => { it('open the modal', async () => {
expect(DeleteModal.methods.show).toHaveBeenCalled(); expect(DeleteModal.methods.show).toHaveBeenCalled();
}); });
it('maps the selection to itemToBeDeleted', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([tagToBeDeleted]);
});
it('tracks a single delete event', () => { it('tracks a single delete event', () => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'registry_tag_delete', label: 'registry_tag_delete',
...@@ -161,18 +205,18 @@ describe('Details Page', () => { ...@@ -161,18 +205,18 @@ describe('Details Page', () => {
}); });
describe('multiple items', () => { describe('multiple items', () => {
beforeEach(() => { beforeEach(async () => {
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags)); mountComponent();
await waitForApolloRequestRender();
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(cleanTags));
}); });
it('open the modal', () => { it('open the modal', () => {
expect(DeleteModal.methods.show).toHaveBeenCalled(); expect(DeleteModal.methods.show).toHaveBeenCalled();
}); });
it('maps the selection to itemToBeDeleted', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual(store.state.tags);
});
it('tracks a single delete event', () => { it('tracks a single delete event', () => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'bulk_registry_tag_delete', label: 'bulk_registry_tag_delete',
...@@ -183,40 +227,77 @@ describe('Details Page', () => { ...@@ -183,40 +227,77 @@ describe('Details Page', () => {
}); });
describe('pagination', () => { describe('pagination', () => {
beforeEach(() => { it('exists', async () => {
mountComponent(); mountComponent();
});
it('exists', () => { await waitForApolloRequestRender();
expect(findPagination().exists()).toBe(true); expect(findPagination().exists()).toBe(true);
}); });
it('is wired to the correct pagination props', () => { it('is hidden when there are no more pages', async () => {
const pagination = findPagination(); mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock) });
expect(pagination.props('perPage')).toBe(store.state.tagsPagination.perPage);
expect(pagination.props('totalItems')).toBe(store.state.tagsPagination.total); await waitForApolloRequestRender();
expect(pagination.props('value')).toBe(store.state.tagsPagination.page);
expect(findPagination().exists()).toBe(false);
}); });
it('fetch the data from the API when the v-model changes', () => { it('is wired to the correct pagination props', async () => {
dispatchSpy.mockResolvedValue(); mountComponent();
findPagination().vm.$emit(GlPagination.model.event, 2);
expect(store.dispatch).toHaveBeenCalledWith('requestTagsList', { await waitForApolloRequestRender();
page: 2,
expect(findPagination().props()).toMatchObject({
hasNextPage: tagsPageInfo.hasNextPage,
hasPreviousPage: tagsPageInfo.hasPreviousPage,
}); });
}); });
it('fetch next page when user clicks next', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
mountComponent({ resolver });
await waitForApolloRequestRender();
findPagination().vm.$emit('next');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: tagsPageInfo.endCursor }),
);
});
it('fetch previous page when user clicks prev', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
mountComponent({ resolver });
await waitForApolloRequestRender();
findPagination().vm.$emit('prev');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }),
);
});
}); });
describe('modal', () => { describe('modal', () => {
it('exists', () => { it('exists', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
expect(findDeleteModal().exists()).toBe(true); expect(findDeleteModal().exists()).toBe(true);
}); });
describe('cancel event', () => { describe('cancel event', () => {
it('tracks cancel_delete', () => { it('tracks cancel_delete', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
findDeleteModal().vm.$emit('cancel'); findDeleteModal().vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
label: 'registry_tag_delete', label: 'registry_tag_delete',
}); });
...@@ -224,45 +305,57 @@ describe('Details Page', () => { ...@@ -224,45 +305,57 @@ describe('Details Page', () => {
}); });
describe('confirmDelete event', () => { describe('confirmDelete event', () => {
let mutationResolver;
beforeEach(() => {
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
mountComponent({ mutationResolver });
return waitForApolloRequestRender();
});
describe('when one item is selected to be deleted', () => { describe('when one item is selected to be deleted', () => {
beforeEach(() => { it('calls apollo mutation with the right parameters', async () => {
mountComponent(); findTagsList().vm.$emit('delete', { [cleanTags[0].name]: true });
findTagsList().vm.$emit('delete', { [store.state.tags[0].name]: true });
}); await wrapper.vm.$nextTick();
it('dispatch requestDeleteTag with the right parameters', () => {
findDeleteModal().vm.$emit('confirmDelete'); findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0], expect(mutationResolver).toHaveBeenCalledWith(
}); expect.objectContaining({ tagNames: [cleanTags[0].name] }),
);
}); });
}); });
describe('when more than one item is selected to be deleted', () => { describe('when more than one item is selected to be deleted', () => {
beforeEach(() => { it('calls apollo mutation with the right parameters', async () => {
mountComponent(); findTagsList().vm.$emit('delete', { ...tagsArrayToSelectedTags(tagsMock) });
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(store.state.tags));
}); await wrapper.vm.$nextTick();
it('dispatch requestDeleteTags with the right parameters', () => {
findDeleteModal().vm.$emit('confirmDelete'); findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name), expect(mutationResolver).toHaveBeenCalledWith(
}); expect.objectContaining({ tagNames: tagsMock.map(t => t.name) }),
);
}); });
}); });
}); });
}); });
describe('Header', () => { describe('Header', () => {
it('exists', () => { it('exists', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
expect(findDetailsHeader().exists()).toBe(true); expect(findDetailsHeader().exists()).toBe(true);
}); });
it('has the correct props', () => { it('has the correct props', async () => {
mountComponent(); mountComponent();
expect(findDetailsHeader().props()).toEqual({ imageName: imageDetailsMock.name });
await waitForApolloRequestRender();
expect(findDetailsHeader().props()).toEqual({ imageName: containerRepositoryMock.name });
}); });
}); });
...@@ -273,13 +366,15 @@ describe('Details Page', () => { ...@@ -273,13 +366,15 @@ describe('Details Page', () => {
}; };
const deleteAlertType = 'success_tag'; const deleteAlertType = 'success_tag';
it('exists', () => { it('exists', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
expect(findDeleteAlert().exists()).toBe(true); expect(findDeleteAlert().exists()).toBe(true);
}); });
it('has the correct props', () => { it('has the correct props', async () => {
store.commit(SET_INITIAL_STATE, { ...config }); store.commit('SET_INITIAL_STATE', { ...config });
mountComponent({ mountComponent({
options: { options: {
data: () => ({ data: () => ({
...@@ -287,6 +382,9 @@ describe('Details Page', () => { ...@@ -287,6 +382,9 @@ describe('Details Page', () => {
}), }),
}, },
}); });
await waitForApolloRequestRender();
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType }); expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
}); });
}); });
...@@ -298,30 +396,40 @@ describe('Details Page', () => { ...@@ -298,30 +396,40 @@ describe('Details Page', () => {
}; };
describe('when expiration_policy_started is not null', () => { describe('when expiration_policy_started is not null', () => {
let resolver;
beforeEach(() => { beforeEach(() => {
store.commit(SET_IMAGE_DETAILS, { resolver = jest.fn().mockResolvedValue(
...imageDetailsMock, graphQLImageDetailsMock({
cleanup_policy_started_at: Date.now().toString(), expirationPolicyStartedAt: Date.now().toString(),
}); }),
);
}); });
it('exists', () => { it('exists', async () => {
mountComponent(); mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true); expect(findPartialCleanupAlert().exists()).toBe(true);
}); });
it('has the correct props', () => { it('has the correct props', async () => {
store.commit(SET_INITIAL_STATE, { ...config }); store.commit('SET_INITIAL_STATE', { ...config });
mountComponent(); mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().props()).toEqual({ ...config }); expect(findPartialCleanupAlert().props()).toEqual({ ...config });
}); });
it('dismiss hides the component', async () => { it('dismiss hides the component', async () => {
mountComponent(); mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(true); expect(findPartialCleanupAlert().exists()).toBe(true);
findPartialCleanupAlert().vm.$emit('dismiss'); findPartialCleanupAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -331,11 +439,22 @@ describe('Details Page', () => { ...@@ -331,11 +439,22 @@ describe('Details Page', () => {
}); });
describe('when expiration_policy_started is null', () => { describe('when expiration_policy_started is null', () => {
it('the component is hidden', () => { it('the component is hidden', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(false); expect(findPartialCleanupAlert().exists()).toBe(false);
}); });
}); });
}); });
describe('Breadcrumb connection', () => {
it('when the details are fetched updates the name', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
});
});
}); });
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