Commit ee3cf5d6 authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Adds tests to vuex and collapsibe component

Formats dates
Fixes clipboard button
Simplifies HTML
parent 6c63520e
......@@ -33,7 +33,7 @@
'fetchList',
'deleteRepo',
'deleteRegistry',
'toggleIsLoading',
'toggleLoading',
]),
fetchRegistryList(repo) {
......@@ -49,7 +49,7 @@
deleteRepository(repo) {
this.deleteRepo(repo)
.then(() => this.fetchRepo())
.then(() => this.fetchRepos())
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
......
......@@ -2,6 +2,7 @@
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
name: 'collapsibeContainerRegisty',
......@@ -15,6 +16,9 @@
clipboardButton,
loadingIcon,
},
mixins: [
timeagoMixin,
],
directives: {
tooltip,
},
......@@ -28,16 +32,18 @@
const pluralize = gl.text.pluralize('layer', item.layers);
return `${item.layers} ${pluralize}`;
},
toggleRepo() {
if (this.isOpen === false) {
// consider not fetching data the second time it is toggled? :fry:
this.$emit('fetchRegistryList', this.repo);
}
this.isOpen = !this.isOpen;
},
handleDeleteRepository() {
this.$emit('deleteRepository', this.repo)
},
handleDeleteRegistry(registry) {
this.$emit('deleteRegistry', this.repo, registry);
},
......@@ -51,7 +57,8 @@
class="container-image-head">
<a
role="button"
@click="toggleRepo">
@click="toggleRepo"
class="js-toggle-repo">
<i
class="fa"
:class="{
......@@ -63,13 +70,17 @@
{{repo.name}}
</a>
<clipboard-button text="foo" title="bar" />
<clipboard-button
v-if="repo.location"
:text="__(`docker pull ${repo.location}`)"
:title="repo.location"
/>
<div class="controls hidden-xs pull-right">
<button
v-if="repo.canDelete"
type="button"
class="btn btn-remove"
class="js-remove-repo btn btn-remove"
:title="__('Remove repository')"
v-tooltip
@click="handleDeleteRepository">
......@@ -90,14 +101,16 @@
v-else-if="!repo.isLoading && isOpen"
class="container-image-tags">
<table class="table tags" v-if="repo.list.length">
<table
class="table tags"
v-if="repo.list.length">
<thead>
<tr>
<th>{{__("Tag")}}</th>
<th>{{__("Tag ID")}}</th>
<th>{{__("Size")}}</th>
<th>{{__("Created")}}</th>
<th v-if="true"></th>
<th></th>
</tr>
</thead>
<tbody>
......@@ -109,16 +122,16 @@
{{item.tag}}
<clipboard-button
:title="item.tag"
:text="item.tag"
v-if="item.location"
:title="item.location"
:text="__(`docker pull ${item.location}`)"
/>
</td>
<td>
<span
v-tooltip
:title="item.revision"
data-placement="bottom"
>
data-placement="bottom">
{{item.shortRevision}}
</span>
</td>
......@@ -128,34 +141,38 @@
&middot;
{{layers(item)}}
</template>
<div v-else class="light">
<div
v-else
class="light">
\-
</div>
</td>
<td>
<template v-if="item.createdAt">
format {{item.createdAt}}
{{timeFormated(item.createdAt)}}
</template>
<div v-else class="light">
<div
v-else
class="light">
\-
</div>
</td>
<td class="content">
<div class="controls hidden-xs pull-right">
<button
type="button"
class="btn btn-remove"
title="Remove tag"
v-tooltip
@click="handleDeleteRegistry(item)">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</div>
<button
v-if="item.canDelete"
type="button"
class="js-delete-registry btn btn-remove hidden-xs pull-right"
:title="__('Remove tag')"
data-container="body"
v-tooltip
@click="handleDeleteRegistry(item)">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</td>
</tr>
</tbody>
......
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import registryApp from './components/app.vue';
// Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-registry-images',
components: {
......
......@@ -27,16 +27,10 @@ export const fetchList = ({ commit }, list) => {
};
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.path)
.then(res => res.json())
.then(() => {
commit(types.DELETE_REPO, repo);
});
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.path)
.then(res => res.json())
.then(() => {
commit(types.DELETE_IMAGE, image);
});
.then(res => res.json());
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleIsLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
\ No newline at end of file
export const repos = state => state.repos;
export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
export const FETCH_REPOS_LIST = 'FETCH_REPOS_LIST';
export const DELETE_REPO = 'DELETE_REPO';
export const SET_REPOS_LIST = 'SET_REPOS_LIST';
export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
export const FETCH_IMAGES_LIST = 'FETCH_IMAGES_LIST';
export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
export const DELETE_IMAGE = 'DELETE_IMAGE';
export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
......@@ -11,12 +11,12 @@ export default {
repos: list.map(el => ({
canDelete: !!el.destroy_path,
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
list: [],
location: el.location,
name: el.name,
tagsPath: el.tags_path,
id: el.id,
})),
});
},
......@@ -26,26 +26,6 @@ export default {
},
[types.SET_REGISTRY_LIST](state, repo, list) {
// mock
list = [
{
name: 'centos6',
short_revision: '0b6091a66',
revision: '0b6091a665af68bbbbb36a3e088ec3cd6f35389deebf6d4617042d56722d76fb',
size: 706,
layers: 19,
created_at: 1505828744434,
},
{
name: 'centos7',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
size: 679,
layers: 19,
created_at: 1505828744434,
},
];
const listToUpdate = state.repos.find(el => el.id === repo.id);
listToUpdate.list = list.map(element => ({
......@@ -54,6 +34,7 @@ export default {
shortRevision: element.short_revision,
size: element.size,
layers: element.layers,
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
canDelete: !!element.destroy_path,
......
<script>
import Clipboard from 'vendor/clipboard';
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
export default {
name: 'clipboardButton',
......@@ -13,13 +15,6 @@
required: true,
},
},
mounted() {
// return new Clipboard(this.$refs.btn, {
// text: () => {
// return this.text;
// },
// });
}
};
</script>
......@@ -28,9 +23,7 @@
type="button"
class="btn btn-transparent btn-clipboard"
:data-title="title"
:data-clipboard-text="text"
ref="btn"
>
:data-clipboard-text="text">
<i
aria-hidden="true"
class="fa fa-clipboard">
......
......@@ -9,10 +9,6 @@
.container-image-head {
padding: 0 16px;
line-height: 4em;
&:hover {
text-decoration: underline;
}
}
.table.tags {
......
......@@ -10,7 +10,7 @@ module Projects
respond_to do |format|
format.html
format.json do
# render json: @images
# Remove code below
render json: [
{
name: 'gitlab-org/omnibus-gitlab/foo',
......@@ -41,13 +41,27 @@ module Projects
def destroy
if image.destroy
redirect_to project_container_registry_index_path(@project),
status: 302,
notice: 'Image repository has been removed successfully!'
respond_to do |format|
# TODO: @Kamil, I don't think this is used ever. Should we keep it or remove it?
format.html do
redirect_to project_container_registry_index_path(@project),
status: 302,
notice: 'Image repository has been removed successfully!'
end
format.json { head :no_content }
end
else
redirect_to project_container_registry_index_path(@project),
status: 302,
alert: 'Failed to remove image repository!'
respond_to do |format|
# TODO: @Kamil, I don't think this is used ever. Should we keep it or remove it?
format.html do
redirect_to project_container_registry_index_path(@project),
status: 302,
alert: 'Failed to remove image repository!'
end
format.json { head :no_content }
end
end
end
......
......@@ -8,7 +8,7 @@
= _('With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
Learn more about
= _('Learn more about')
= link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
.row
.col-lg-12
......@@ -20,14 +20,14 @@
%p
= _('First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have')
= link_to _('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
you need to use a
= _('you need to use a')
= succeed ':' do
= link_to _('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
= _("Once you log in, you&rsquo;re free to create and upload a container image using the common")
= _('Once you log in, you&rsquo;re free to create and upload a container image using the common')
%code
= _('build')
= _('and')
......@@ -37,7 +37,6 @@
:plain
docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}
%hr
%h5.prepend-top-default
= _('Use different image names')
......@@ -48,8 +47,6 @@
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
.row
.col-lg-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json)}}
......
import * as actions from '~/notes/stores/actions';
import testAction from './helpers';
import testAction from '../../helpers/vuex_action_helper';
import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
......
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('collapsible registry container', () => {
let vm;
let Component;
let mockData;
beforeEach(() => {
Component = Vue.extend(collapsibleComponent);
mockData = {
canDelete: true,
destroyPath: 'path',
id: '123',
isLoading: false,
list: [
{
tag: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
shortRevision: 'b118ab5b0',
size: 19,
layers: 10,
location: 'location',
createdAt: 1505828744434,
destroyPath: 'path',
canDelete: true,
},
],
location: 'location',
name: 'foo',
tagsPath: 'path',
};
vm = mountComponent(Component, { repo: mockData });
});
afterEach(() => {
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
});
it('should be open when user clicks on closed repo', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up');
done();
});
});
it('should be closed when the user clicks on an opened repo', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
done();
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
});
});
describe('registry list', () => {
it('should render a table with the registry list', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelectorAll('table tbody tr').length,
).toEqual(mockData.list.length);
done();
});
});
it('should render registry tag', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' ');
expect(textRendered).toContain(mockData.list[0].tag);
expect(textRendered).toContain(mockData.list[0].shortRevision);
expect(textRendered).toContain(mockData.list[0].layers);
expect(textRendered).toContain(mockData.list[0].size);
done();
});
});
it('should be possible to delete a registry', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.table tbody tr .js-delete-registry'),
).toBeDefined();
done();
});
});
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
import _ from 'underscore';
import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types';
import testAction from '../../helpers/vuex_action_helper';
import {
defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
} from './mock_data';
Vue.use(VueResource);
describe('Actions Registry Store', () => {
let interceptor;
let mockedState;
beforeEach(() => {
mockedState = defaultState;
});
describe('server requests', () => {
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
describe('fetchRepos', () => {
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
});
it('should set receveived repos', (done) => {
testAction(actions.fetchRepos, null, mockedState, [
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
], done);
});
});
describe('fetchList', () => {
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(registryServerResponse), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
});
it('should set received list', (done) => {
mockedState.repos = parsedReposServerResponse;
testAction(actions.fetchList, mockedState.repos[1], mockedState, [
{ type: types.TOGGLE_REGISTRY_LIST_LOADING },
{ type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
], done);
});
});
});
describe('setMainEndpoint', () => {
it('should commit set main endpoint', (done) => {
testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
], done);
});
});
describe('toggleLoading', () => {
it('should commit toggle main loading', (done) => {
testAction(actions.toggleLoading, null, mockedState, [
{ type: types.TOGGLE_MAIN_LOADING },
], done);
});
});
});
import * as getters from '~/registry/stores/getters';
describe('Getters Registry Store', () => {
let state;
beforeEach(() => {
state = {
isLoading: false,
endpoint: '/root/empty-project/container_registry.json',
repos: [{
canDelete: true,
destroyPath: 'bar',
id: '134',
isLoading: false,
list: [],
location: 'foo',
name: 'gitlab-org/omnibus-gitlab/foo',
tagsPath: 'foo',
}, {
canDelete: true,
destroyPath: 'bar',
id: '123',
isLoading: false,
list: [],
location: 'foo',
name: 'gitlab-org/omnibus-gitlab',
tagsPath: 'foo',
}],
};
});
describe('isLoading', () => {
it('should return the isLoading property', () => {
expect(getters.isLoading(state)).toEqual(state.isLoading);
});
});
describe('repos', () => {
it('should return the repos', () => {
expect(getters.repos(state)).toEqual(state.repos);
});
});
});
export const defaultState = {
isLoading: false,
endpoint: '',
repos: [],
};
export const reposServerResponse = [
{
destroy_path: 'path',
id: '123',
location: 'location',
name: 'foo',
tags_path: 'tags_path',
},
{
destroy_path: 'path_',
id: '456',
location: 'location_',
name: 'bar',
tags_path: 'tags_path_',
},
];
export const registryServerResponse = [
{
name: 'centos7',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
destroy_path: 'path_',
},
{
name: 'centos6',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
}];
export const parsedReposServerResponse = [
{
canDelete: true,
destroyPath: reposServerResponse[0].destroy_path,
id: reposServerResponse[0].id,
isLoading: false,
list: [],
location: reposServerResponse[0].location,
name: reposServerResponse[0].name,
tagsPath: reposServerResponse[0].tags_path,
},
{
canDelete: true,
destroyPath: reposServerResponse[1].destroy_path,
id: reposServerResponse[1].id,
isLoading: false,
list: [],
location: reposServerResponse[1].location,
name: reposServerResponse[1].name,
tagsPath: reposServerResponse[1].tags_path,
},
];
export const parsedRegistryServerResponse = [
{
tag: registryServerResponse[0].name,
revision: registryServerResponse[0].revision,
shortRevision: registryServerResponse[0].short_revision,
size: registryServerResponse[0].size,
layers: registryServerResponse[0].layers,
location: registryServerResponse[0].location,
createdAt: registryServerResponse[0].created_at,
destroyPath: registryServerResponse[0].destroy_path,
canDelete: true,
},
{
tag: registryServerResponse[1].name,
revision: registryServerResponse[1].revision,
shortRevision: registryServerResponse[1].short_revision,
size: registryServerResponse[1].size,
layers: registryServerResponse[1].layers,
location: registryServerResponse[1].location,
createdAt: registryServerResponse[1].created_at,
destroyPath: registryServerResponse[1].destroy_path,
canDelete: false,
},
];
import mutations from '~/registry/stores/mutations';
import * as types from '~/registry/stores/mutation_types';
import {
defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
parsedRegistryServerResponse,
} from './mock_data';
describe('Mutations Registry Store', () => {
let mockState;
beforeEach(() => {
mockState = defaultState;
});
describe('SET_MAIN_ENDPOINT', () => {
it('should set the main endpoint', () => {
const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
expect(mockState).toEqual(expectedState);
});
});
describe('SET_REPOS_LIST', () => {
it('should set a parsed repository list', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
expect(mockState.repos).toEqual(parsedReposServerResponse);
});
});
describe('TOGGLE_MAIN_LOADING', () => {
it('should set a parsed repository list', () => {
mutations[types.TOGGLE_MAIN_LOADING](mockState);
expect(mockState.isLoading).toEqual(true);
});
});
describe('SET_REGISTRY_LIST', () => {
it('should set a list of registries in a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
mutations[types.SET_REGISTRY_LIST](mockState, mockState.repos[0], registryServerResponse);
expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse);
});
});
describe('TOGGLE_REGISTRY_LIST_LOADING', () => {
it('should toggle isLoading property for a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
mutations[types.SET_REGISTRY_LIST](mockState, mockState.repos[0], registryServerResponse);
mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]);
expect(mockState.repos[0].isLoading).toEqual(true);
});
});
});
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