Commit 63a79970 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '23315-details-and-list-page' into 'master'

List and Details page for container registry explorer

See merge request gitlab-org/gitlab!23154
parents 833a65e5 3d39cbf5
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
export default {
name: 'GroupEmptyState',
components: {
GlEmptyState,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config']),
},
};
</script>
<template>
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images available in this group')"
:svg-path="config.noContainersImage"
class="container-message"
>
<template #description>
<p class="js-no-container-images-text">
<gl-sprintf
:message="
s__(
`ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-empty-state>
</template>
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { mapState } from 'vuex';
export default {
name: 'ProjectEmptyState',
components: {
ClipboardButton,
GlEmptyState,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config']),
dockerBuildCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker build -t ${this.config.repositoryUrl} .`;
},
dockerPushCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker push ${this.config.repositoryUrl}`;
},
dockerLoginCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker login ${this.config.registryHostUrlWithPort}`;
},
},
};
</script>
<template>
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="config.noContainersImage"
class="container-message"
>
<template #description>
<p class="js-no-container-images-text">
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
<p class="js-not-logged-in-to-registry-text">
<gl-sprintf
:message="
s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
the Container Registry by using your GitLab username and password. If you have
%{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
%{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
instead of a password.`)
"
>
<template #twofaDocLink="{content}">
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
</template>
<template #personalAccessTokensDocLink="{content}">
<gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
<div class="input-group append-bottom-10">
<input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerLoginCommand"
:title="s__('ContainerRegistry|Copy login command')"
class="input-group-text"
/>
</span>
</div>
<p></p>
<p>
{{
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p>
<div class="input-group append-bottom-10">
<input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command')"
class="input-group-text"
/>
</span>
</div>
<div class="input-group">
<input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command')"
class="input-group-text"
/>
</span>
</div>
</template>
</gl-empty-state>
</template>
<script> <script>
export default {}; import { mapState, mapActions } from 'vuex';
import {
GlLoadingIcon,
GlEmptyState,
GlPagination,
GlTooltipDirective,
GlButton,
GlIcon,
GlModal,
GlSprintf,
GlLink,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
export default {
name: 'RegistryListApp',
components: {
GlEmptyState,
GlLoadingIcon,
GlPagination,
ProjectEmptyState,
GroupEmptyState,
ClipboardButton,
GlButton,
GlIcon,
GlModal,
GlSprintf,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
data() {
return {
itemToDelete: {},
};
},
computed: {
...mapState(['config', 'isLoading', 'images', 'pagination']),
tracking() {
return {
label: 'registry_repository_delete',
};
},
currentPage: {
get() {
return this.pagination.page;
},
set(page) {
this.requestImagesList({ page });
},
},
},
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
deleteImage(item) {
// This event is already tracked in the system and so the name must be kept to aggregate the data
this.track('click_button');
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
handleDeleteRepository() {
this.track('confirm_delete');
this.requestDeleteImage(this.itemToDelete.destroy_path);
this.itemToDelete = {};
},
encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path });
return window.btoa(params);
},
},
};
</script> </script>
<template> <template>
<div></div> <div class="position-absolute w-100 slide-enter-from-element">
<gl-empty-state
v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')"
:svg-path="config.containersErrorImage"
>
<template #description>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-empty-state>
<template v-else>
<gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
<template v-else>
<div v-if="images.length" ref="imagesList">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<div class="d-flex flex-column">
<div
v-for="(listItem, index) in images"
:key="index"
ref="rowItem"
:class="[
'd-flex justify-content-between align-items-center py-2 border-bottom',
{ 'border-top': index === 0 },
]"
>
<div>
<router-link
ref="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
)
"
>
<gl-button
ref="deleteImageButton"
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
>
<gl-icon name="remove" />
</gl-button>
</div>
</div>
</div>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 mt-2"
/>
</div>
<template v-else>
<project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else />
</template>
</template>
<gl-modal
ref="deleteModal"
modal-id="delete-image-modal"
ok-variant="danger"
@ok="handleDeleteRepository"
@cancel="track('cancel_delete')"
>
<template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p>
<gl-sprintf
:message=" s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
),"
>
<template #title>
<b>{{ itemToDelete.path }}</b>
</template>
</gl-sprintf>
</p>
<template #modal-ok>{{ __('Remove') }}</template>
</gl-modal>
</template>
</div>
</template> </template>
...@@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) ...@@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {})
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => { export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
const url = window.atob(id); const { tags_path } = JSON.parse(window.atob(id));
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination; const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios return axios
.get(url, { params: { page, per_page: perPage } }) .get(tags_path, { params: { page, per_page: perPage } })
.then(({ data, headers }) => { .then(({ data, headers }) => {
dispatch('receiveTagsListSuccess', { data, headers }); dispatch('receiveTagsListSuccess', { data, headers });
}) })
......
...@@ -5,6 +5,7 @@ export default { ...@@ -5,6 +5,7 @@ export default {
[types.SET_INITIAL_STATE](state, config) { [types.SET_INITIAL_STATE](state, config) {
state.config = { state.config = {
...config, ...config,
isGroupPage: config.isGroupPage !== undefined,
}; };
}, },
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
is_group_page: true,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
- else - else
#js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json), #js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json),
......
...@@ -5071,6 +5071,9 @@ msgstr "" ...@@ -5071,6 +5071,9 @@ msgstr ""
msgid "Container repositories sync capacity" msgid "Container repositories sync capacity"
msgstr "" msgstr ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept." msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "" msgstr ""
...@@ -5122,6 +5125,9 @@ msgstr "" ...@@ -5122,6 +5125,9 @@ msgstr ""
msgid "ContainerRegistry|Last Updated" msgid "ContainerRegistry|Last Updated"
msgstr "" msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr ""
msgid "ContainerRegistry|Number of tags to retain:" msgid "ContainerRegistry|Number of tags to retain:"
msgstr "" msgstr ""
...@@ -5187,12 +5193,21 @@ msgstr "" ...@@ -5187,12 +5193,21 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
msgstr ""
msgid "ContainerRegistry|You are about to remove %{item}. Are you sure?"
msgstr ""
msgid "ContainerRegistry|You are about to remove <b>%{count}</b> tags. Are you sure?" msgid "ContainerRegistry|You are about to remove <b>%{count}</b> tags. Are you sure?"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove <b>%{title}</b>. Are you sure?" msgid "ContainerRegistry|You are about to remove <b>%{title}</b>. Are you sure?"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted."
msgstr ""
msgid "ContainerRegistry|You are about to remove repository <b>%{title}</b>. Once you confirm, this repository will be permanently deleted." msgid "ContainerRegistry|You are about to remove repository <b>%{title}</b>. Once you confirm, this repository will be permanently deleted."
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Group Empty state to match the default snapshot 1`] = `
<div
class="container-message"
svg-path="foo"
title="There are no container images available in this group"
>
<p
class="js-no-container-images-text"
>
With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
<gl-link-stub
href="baz"
target="_blank"
>
More Information
</gl-link-stub>
</p>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Project Empty state to match the default snapshot 1`] = `
<div
class="container-message"
svg-path="bazFoo"
title="There are no container images stored for this project"
>
<p
class="js-no-container-images-text"
>
With the Container Registry, every project can have its own space to store its Docker images.
<gl-link-stub
href="baz"
target="_blank"
>
More Information
</gl-link-stub>
</p>
<h5>
Quick Start
</h5>
<p
class="js-not-logged-in-to-registry-text"
>
If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
<gl-link-stub
href="barBaz"
target="_blank"
>
Two-Factor Authentication
</gl-link-stub>
enabled, use a
<gl-link-stub
href="fooBaz"
target="_blank"
>
Personal Access Token
</gl-link-stub>
instead of a password.
</p>
<div
class="input-group append-bottom-10"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker login bar"
title="Copy login command"
tooltipplacement="top"
/>
</span>
</div>
<p />
<p>
You can add an image to this registry with the following commands:
</p>
<div
class="input-group append-bottom-10"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker build -t foo ."
title="Copy build command"
tooltipplacement="top"
/>
</span>
</div>
<div
class="input-group"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker push foo"
title="Copy push command"
tooltipplacement="top"
/>
</span>
</div>
</div>
`;
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs';
import groupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Group Empty state', () => {
let wrapper;
let store;
beforeEach(() => {
store = new Vuex.Store({
state: {
config: {
noContainersImage: 'foo',
helpPagePath: 'baz',
},
},
});
wrapper = shallowMount(groupEmptyState, {
localVue,
store,
stubs: {
GlEmptyState,
GlSprintf,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs';
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Project Empty state', () => {
let wrapper;
let store;
beforeEach(() => {
store = new Vuex.Store({
state: {
config: {
repositoryUrl: 'foo',
registryHostUrlWithPort: 'bar',
helpPagePath: 'baz',
twoFactorAuthHelpLink: 'barBaz',
personalAccessTokensHelpLink: 'fooBaz',
noContainersImage: 'bazFoo',
},
},
});
wrapper = shallowMount(projectEmptyState, {
localVue,
store,
stubs: {
GlEmptyState,
GlSprintf,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
export const headers = {
'X-PER-PAGE': 5,
'X-PAGE': 1,
'X-TOTAL': 13,
'X-TOTAL_PAGES': 1,
'X-NEXT-PAGE': null,
'X-PREVIOUS-PAGE': null,
};
export const reposServerResponse = [ export const reposServerResponse = [
{ {
destroy_path: 'path', destroy_path: 'path',
...@@ -36,3 +44,46 @@ export const registryServerResponse = [ ...@@ -36,3 +44,46 @@ export const registryServerResponse = [
created_at: 1505828744434, created_at: 1505828744434,
}, },
]; ];
export const imagesListResponse = {
data: [
{
path: 'foo',
location: 'location',
destroy_path: 'path',
},
{
path: 'bar',
location: 'location-2',
destroy_path: 'path-2',
},
],
headers,
};
export const tagsListResponse = {
data: [
{
tag: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
short_revision: 'b118ab5b0',
size: 19,
layers: 10,
location: 'location',
path: 'bar',
created_at: 1505828744434,
destroy_path: 'path',
},
{
tag: 'test-image',
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
short_revision: 'b969de599',
size: 19,
layers: 10,
path: 'foo',
location: 'location-2',
created_at: 1505828744434,
},
],
headers,
};
This diff is collapsed.
import VueRouter from 'vue-router';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { imagesListResponse } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('List Page', () => {
let wrapper;
let dispatchSpy;
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
const findEmptyState = () => wrapper.find(GlEmptyState);
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
const findPagination = () => wrapper.find(GlPagination);
beforeEach(() => {
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlModal,
GlEmptyState,
GlSprintf,
},
});
dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveImagesListSuccess', imagesListResponse);
});
afterEach(() => {
wrapper.destroy();
});
describe('connection error', () => {
const config = {
characterError: true,
containersErrorImage: 'foo',
helpPagePath: 'bar',
};
beforeAll(() => {
store.dispatch('setInitialState', config);
});
afterAll(() => {
store.dispatch('setInitialState', {});
});
it('should show an empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('empty state should have an svg-path', () => {
expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
});
it('empty state should have a description', () => {
expect(findEmptyState().html()).toContain('connection error');
});
it('should not show the loading or default state', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false);
});
});
describe('when isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('shows the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false);
});
});
describe('list', () => {
describe('listElement', () => {
let listElements;
let firstElement;
beforeEach(() => {
listElements = findRowItems();
[firstElement] = store.state.images;
});
it('contains one list element for each image', () => {
expect(listElements.length).toBe(store.state.images.length);
});
it('contains a link to the details page', () => {
const link = findDetailsLink();
expect(link.html()).toContain(firstElement.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(firstElement.location);
expect(button.props('title')).toBe(firstElement.location);
});
describe('delete image', () => {
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn();
expect(deleteBtn.exists()).toBe(true);
});
it('should call deleteItem when confirming deletion', () => {
dispatchSpy.mockResolvedValue();
const itemToDelete = wrapper.vm.images[0];
wrapper.setData({ itemToDelete });
findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
itemToDelete.destroy_path,
);
});
});
});
describe('pagination', () => {
it('exists', () => {
expect(findPagination().exists()).toBe(true);
});
it('is wired to the correct pagination props', () => {
const pagination = findPagination();
expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
expect(pagination.props('value')).toBe(store.state.pagination.page);
});
it('fetch the data from the API when the v-model changes', () => {
dispatchSpy.mockReturnValue();
wrapper.setData({ currentPage: 2 });
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
});
});
});
});
describe('modal', () => {
it('exists', () => {
expect(findDeleteModal().exists()).toBe(true);
});
it('contains a description with the path of the item to delete', () => {
wrapper.setData({ itemToDelete: { path: 'foo' } });
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain('foo');
});
});
});
describe('tracking', () => {
const testTrackingCall = action => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
label: 'registry_repository_delete',
});
};
beforeEach(() => {
jest.spyOn(Tracking, 'event');
dispatchSpy.mockReturnValue();
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteBtn();
deleteBtn.vm.$emit('click');
testTrackingCall('click_button');
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
testTrackingCall('cancel_delete');
});
it('send an event when confirm is clicked on modal', () => {
dispatchSpy.mockReturnValue();
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
testTrackingCall('confirm_delete');
});
});
});
});
...@@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => {
}); });
describe('fetch tags list', () => { describe('fetch tags list', () => {
const url = window.btoa(`${endpoint}/1}`); const url = `${endpoint}/1}`;
const path = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` }));
it('sets the tagsList', done => { it('sets the tagsList', done => {
mock.onGet(window.atob(url)).replyOnce(200, registryServerResponse, {}); mock.onGet(url).replyOnce(200, registryServerResponse, {});
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: url }, { id: path },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
...@@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
it('should create flash on error', done => { it('should create flash on error', done => {
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: url }, { id: path },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
......
...@@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const expectedState = { ...mockState, config: { endpoint: 'foo' } }; const payload = { endpoint: 'foo', isGroupPage: true };
mutations[types.SET_INITIAL_STATE](mockState, { endpoint: 'foo' }); const expectedState = { ...mockState, config: payload };
mutations[types.SET_INITIAL_STATE](mockState, payload);
expect(mockState).toEqual(expectedState); expect(mockState).toEqual(expectedState);
}); });
......
export const GlModal = {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
methods: {
show: jest.fn(),
},
};
export const GlEmptyState = {
template: '<div><slot name="description"></slot></div>',
name: 'GlEmptyStateSTub',
};
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