Commit c2496044 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Fatih Acet

Unit test for collapsible container

Reachable code fully covered
parent 7c13f24e
...@@ -47,7 +47,7 @@ export default { ...@@ -47,7 +47,7 @@ export default {
dockerConnectionErrorText() { dockerConnectionErrorText() {
return sprintf( return sprintf(
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path. issue with your project name or path.
%{docLinkStart}More Information%{docLinkEnd}`), %{docLinkStart}More Information%{docLinkEnd}`),
{ {
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`, docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`,
...@@ -58,8 +58,8 @@ export default { ...@@ -58,8 +58,8 @@ export default {
}, },
introText() { introText() {
return sprintf( return sprintf(
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images. project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`), %{docLinkStart}More Information%{docLinkEnd}`),
{ {
docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
:svg-path="containersErrorImage" :svg-path="containersErrorImage"
> >
<template #description> <template #description>
<p v-html="dockerConnectionErrorText"></p> <p class="js-character-error-text" v-html="dockerConnectionErrorText"></p>
</template> </template>
</gl-empty-state> </gl-empty-state>
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
} }
}, },
handleDeleteRepository() { handleDeleteRepository() {
this.deleteItem(this.repo) return this.deleteItem(this.repo)
.then(() => { .then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice'); createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos(); this.fetchRepos();
...@@ -67,7 +67,8 @@ export default { ...@@ -67,7 +67,8 @@ export default {
<div class="container-image"> <div class="container-image">
<div class="container-image-head"> <div class="container-image-head">
<gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo"> <gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
<icon :name="iconName" /> {{ repo.name }} <icon :name="iconName" />
{{ repo.name }}
</gl-button> </gl-button>
<clipboard-button <clipboard-button
......
...@@ -198,8 +198,9 @@ export default { ...@@ -198,8 +198,9 @@ export default {
:title="s__('ContainerRegistry|Remove selected images')" :title="s__('ContainerRegistry|Remove selected images')"
:aria-label="s__('ContainerRegistry|Remove selected images')" :aria-label="s__('ContainerRegistry|Remove selected images')"
@click="deleteMultipleItems()" @click="deleteMultipleItems()"
><icon name="remove" >
/></gl-button> <icon name="remove" />
</gl-button>
</th> </th>
</tr> </tr>
</thead> </thead>
...@@ -223,9 +224,9 @@ export default { ...@@ -223,9 +224,9 @@ export default {
/> />
</td> </td>
<td> <td>
<span v-gl-tooltip.bottom class="monospace" :title="item.revision"> <span v-gl-tooltip.bottom class="monospace" :title="item.revision">{{
{{ item.shortRevision }} item.shortRevision
</span> }}</span>
</td> </td>
<td> <td>
{{ formatSize(item.size) }} {{ formatSize(item.size) }}
...@@ -236,9 +237,9 @@ export default { ...@@ -236,9 +237,9 @@ export default {
</td> </td>
<td> <td>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)"> <span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
{{ timeFormated(item.createdAt) }} timeFormated(item.createdAt)
</span> }}</span>
</td> </td>
<td class="content action-buttons"> <td class="content action-buttons">
...@@ -262,6 +263,7 @@ export default { ...@@ -262,6 +263,7 @@ export default {
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="onPageChange" :change="onPageChange"
:page-info="repo.pagination" :page-info="repo.pagination"
class="js-registry-pagination"
/> />
<gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger"> <gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
......
import registry from '~/registry/components/app.vue';
import { mount } from '@vue/test-utils';
import { TEST_HOST } from '../../helpers/test_constants';
import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
describe('Registry List', () => {
let wrapper;
const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
const findNoContainerImagesText = w => w.find('.js-no-container-images-text');
const findSpinner = w => w.find('.gl-spinner');
const findCharacterErrorText = w => w.find('.js-character-error-text');
const propsData = {
endpoint: `${TEST_HOST}/foo`,
helpPagePath: 'foo',
noContainersImage: 'foo',
containersErrorImage: 'foo',
repositoryUrl: 'foo',
};
const setMainEndpoint = jest.fn();
const fetchRepos = jest.fn();
const methods = {
setMainEndpoint,
fetchRepos,
};
beforeEach(() => {
wrapper = mount(registry, {
propsData,
computed: {
repos() {
return parsedReposServerResponse;
},
},
methods,
});
});
describe('with data', () => {
it('should render a list of CollapsibeContainerRegisty', () => {
const containers = findCollapsibleContainer(wrapper);
expect(wrapper.vm.repos.length).toEqual(reposServerResponse.length);
expect(containers.length).toEqual(reposServerResponse.length);
});
});
describe('without data', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData,
computed: {
repos() {
return [];
},
},
methods,
});
});
it('should render empty message', () => {
const noContainerImagesText = findNoContainerImagesText(localWrapper);
expect(noContainerImagesText.text()).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
});
});
describe('while loading data', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData,
computed: {
repos() {
return [];
},
isLoading() {
return true;
},
},
methods,
});
});
it('should render a loading spinner', () => {
const spinner = findSpinner(localWrapper);
expect(spinner.exists()).toBe(true);
});
});
describe('invalid characters in path', () => {
let localWrapper;
beforeEach(() => {
localWrapper = mount(registry, {
propsData: {
...propsData,
characterError: true,
},
computed: {
repos() {
return [];
},
},
methods,
});
});
it('should render invalid characters error message', () => {
const characterErrorText = findCharacterErrorText(localWrapper);
expect(characterErrorText.text()).toEqual(
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More Information',
);
});
});
});
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import { repoPropsData } from '../mock_data';
import createFlash from '~/flash';
jest.mock('~/flash.js');
describe('collapsible registry container', () => {
let wrapper;
const findDeleteBtn = w => w.find('.js-remove-repo');
const findContainerImageTags = w => w.find('.container-image-tags');
const findToggleRepos = w => w.findAll('.js-toggle-repo');
beforeEach(() => {
createFlash.mockClear();
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
wrapper = mount(collapsibleComponent, {
propsData: {
repo: repoPropsData,
},
});
});
afterEach(() => {
Vue.config.silent = false;
});
describe('toggle', () => {
beforeEach(() => {
const fetchList = jest.fn();
wrapper.setMethods({ fetchList });
});
const expectIsClosed = () => {
const container = findContainerImageTags(wrapper);
expect(container.exists()).toBe(false);
expect(wrapper.vm.iconName).toEqual('angle-right');
};
it('should be closed by default', () => {
expectIsClosed();
});
it('should be open when user clicks on closed repo', () => {
const toggleRepos = findToggleRepos(wrapper);
toggleRepos.at(0).trigger('click');
const container = findContainerImageTags(wrapper);
expect(container.exists()).toBe(true);
expect(wrapper.vm.fetchList).toHaveBeenCalled();
});
it('should be closed when the user clicks on an opened repo', done => {
const toggleRepos = findToggleRepos(wrapper);
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
expectIsClosed();
done();
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn(wrapper);
expect(deleteBtn.exists()).toBe(true);
});
it('should call deleteItem when confirming deletion', () => {
const deleteItem = jest.fn().mockResolvedValue();
const fetchRepos = jest.fn().mockResolvedValue();
wrapper.setMethods({ deleteItem, fetchRepos });
wrapper.vm.handleDeleteRepository();
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(wrapper.vm.repo);
});
it('should show an error when there is API error', () => {
const deleteItem = jest.fn().mockRejectedValue('error');
wrapper.setMethods({ deleteItem });
return wrapper.vm.handleDeleteRepository().then(() => {
expect(createFlash).toHaveBeenCalled();
});
});
});
});
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import { mount } from '@vue/test-utils';
import { repoPropsData } from '../mock_data';
const [firstImage, secondImage] = repoPropsData.list;
describe('table registry', () => {
let wrapper;
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
const findDeleteButton = w => w.find('.js-delete-registry');
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
const findPagination = w => w.find('.js-registry-pagination');
const bulkDeletePath = 'path';
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
wrapper = mount(tableRegistry, {
propsData: {
repo: repoPropsData,
},
});
});
afterEach(() => {
Vue.config.silent = false;
});
describe('rendering', () => {
it('should render a table with the registry list', () => {
expect(wrapper.findAll('.registry-image-row').length).toEqual(repoPropsData.list.length);
});
it('should render registry tag', () => {
const tds = wrapper.findAll('.registry-image-row td');
expect(tds.at(0).classes()).toContain('check');
expect(tds.at(1).html()).toContain(repoPropsData.list[0].tag);
expect(tds.at(2).html()).toContain(repoPropsData.list[0].shortRevision);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].layers);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].size);
expect(tds.at(4).html()).toContain(wrapper.vm.timeFormated(repoPropsData.list[0].createdAt));
});
});
describe('multi select', () => {
it('selecting a row should enable delete button', done => {
const deleteBtn = findDeleteButton(wrapper);
const checkboxes = findSelectCheckboxes(wrapper);
expect(deleteBtn.attributes('disabled')).toBe('disabled');
checkboxes.at(0).trigger('click');
Vue.nextTick(() => {
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
const selectAll = findSelectAllCheckbox(wrapper);
const checkboxes = findSelectCheckboxes(wrapper);
selectAll.trigger('click');
Vue.nextTick(() => {
const checked = checkboxes.filter(w => w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
const checkboxes = findSelectCheckboxes(wrapper);
const selectAll = findSelectAllCheckbox(wrapper);
selectAll.trigger('click');
selectAll.trigger('click');
Vue.nextTick(() => {
const checked = checkboxes.filter(w => !w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
const multiDeleteItems = jest.fn().mockResolvedValue();
wrapper.setMethods({ multiDeleteItems });
const selectAll = findSelectAllCheckbox(wrapper);
selectAll.trigger('click');
Vue.nextTick(() => {
const deleteBtn = findDeleteButton(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
wrapper.vm.handleMultipleDelete();
Vue.nextTick(() => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
it('should show an error message if bulkDeletePath is not set', () => {
const showError = jest.fn();
wrapper.setMethods({ showError });
wrapper.setProps({
repo: {
...repoPropsData,
tagsPath: null,
},
});
wrapper.vm.handleMultipleDelete();
expect(wrapper.vm.showError).toHaveBeenCalled();
});
});
describe('delete registry', () => {
beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0] });
});
it('should be possible to delete a registry', () => {
const deleteBtn = findDeleteButton(wrapper);
const deleteBtns = findDeleteButtonsRow(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0]);
expect(deleteBtn).toBeDefined();
expect(deleteBtn.attributes('disable')).toBe(undefined);
expect(deleteBtns.is('button')).toBe(true);
});
it('should allow deletion row by row', () => {
const deleteBtns = findDeleteButtonsRow(wrapper);
const deleteSingleItem = jest.fn();
const deleteItem = jest.fn().mockResolvedValue();
wrapper.setMethods({ deleteSingleItem, deleteItem });
deleteBtns.at(0).trigger('click');
expect(wrapper.vm.deleteSingleItem).toHaveBeenCalledWith(0);
wrapper.vm.handleSingleDelete(1);
expect(wrapper.vm.deleteItem).toHaveBeenCalledWith(1);
});
});
describe('pagination', () => {
let localWrapper = null;
const repo = {
repoPropsData,
pagination: {
total: 20,
perPage: 2,
nextPage: 2,
},
};
beforeEach(() => {
localWrapper = mount(tableRegistry, {
propsData: {
repo,
},
});
});
it('should exist', () => {
const pagination = findPagination(localWrapper);
expect(pagination.exists()).toBe(true);
});
it('should be visible when pagination is needed', () => {
const pagination = findPagination(localWrapper);
expect(pagination.isVisible()).toBe(true);
localWrapper.setProps({
repo: {
pagination: {
total: 0,
perPage: 10,
},
},
});
expect(localWrapper.vm.shouldRenderPagination).toBe(false);
});
it('should have a change function that update the list when run', () => {
const fetchList = jest.fn().mockResolvedValue();
localWrapper.setMethods({ fetchList });
localWrapper.vm.onPageChange(1);
expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
});
});
describe('modal content', () => {
it('should show the singular title and image name when deleting a single image', () => {
wrapper.setData({ itemsToBeDeleted: [1] });
wrapper.vm.setModalDescription(0);
expect(wrapper.vm.modalTitle).toBe('Remove image');
expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
});
it('should show the plural title and image count when deleting more than one image', () => {
wrapper.setData({ itemsToBeDeleted: [1, 2] });
wrapper.vm.setModalDescription();
expect(wrapper.vm.modalTitle).toBe('Remove images');
expect(wrapper.vm.modalDescription).toContain('<b>2</b> images');
});
});
});
...@@ -2,81 +2,121 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,81 +2,121 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as actions from '~/registry/stores/actions'; import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types'; import * as types from '~/registry/stores/mutation_types';
import state from '~/registry/stores/state'; import { TEST_HOST } from '../../helpers/test_constants';
import { TEST_HOST } from 'spec/test_constants';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import createFlash from '~/flash';
import { import {
reposServerResponse, reposServerResponse,
registryServerResponse, registryServerResponse,
parsedReposServerResponse, parsedReposServerResponse,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/flash.js');
describe('Actions Registry Store', () => { describe('Actions Registry Store', () => {
let mockedState;
let mock; let mock;
let state;
beforeEach(() => { beforeEach(() => {
mockedState = state();
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
state = {
endpoint: `${TEST_HOST}/endpoint.json`,
};
}); });
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); });
describe('server requests', () => { describe('fetchRepos', () => {
describe('fetchRepos', () => { beforeEach(() => {
beforeEach(() => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {}); });
});
it('should set receveived repos', done => { it('should set receveived repos', done => {
testAction( testAction(
actions.fetchRepos, actions.fetchRepos,
null, null,
mockedState, state,
[ [
{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING },
{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING },
{ type: types.SET_REPOS_LIST, payload: reposServerResponse }, { type: types.SET_REPOS_LIST, payload: reposServerResponse },
], ],
[], [],
done, done,
); );
});
}); });
describe('fetchList', () => { it('should create flash on API error', done => {
let repo; testAction(
beforeEach(() => { actions.fetchRepos,
mockedState.repos = parsedReposServerResponse; null,
[, repo] = mockedState.repos; {
endpoint: null,
},
[{ type: types.TOGGLE_MAIN_LOADING }, { type: types.TOGGLE_MAIN_LOADING }],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {}); describe('fetchList', () => {
}); let repo;
beforeEach(() => {
state.repos = parsedReposServerResponse;
[, repo] = state.repos;
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
it('should set received list', done => { it('should set received list', done => {
testAction( testAction(
actions.fetchList, actions.fetchList,
{ repo }, { repo },
mockedState, state,
[ [
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo }, { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo }, { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
{ {
type: types.SET_REGISTRY_LIST, type: types.SET_REGISTRY_LIST,
payload: { payload: {
repo, repo,
resp: registryServerResponse, resp: registryServerResponse,
headers: jasmine.anything(), headers: expect.anything(),
},
}, },
], },
[], ],
done, [],
); done,
}); );
});
it('should create flash on API error', done => {
const updatedRepo = {
...repo,
tagsPath: null,
};
testAction(
actions.fetchList,
{
repo: updatedRepo,
},
state,
[
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
{ type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: updatedRepo },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
}); });
}); });
...@@ -85,7 +125,7 @@ describe('Actions Registry Store', () => { ...@@ -85,7 +125,7 @@ describe('Actions Registry Store', () => {
testAction( testAction(
actions.setMainEndpoint, actions.setMainEndpoint,
'endpoint', 'endpoint',
mockedState, state,
[{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }], [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
[], [],
done, done,
...@@ -98,7 +138,7 @@ describe('Actions Registry Store', () => { ...@@ -98,7 +138,7 @@ describe('Actions Registry Store', () => {
testAction( testAction(
actions.toggleLoading, actions.toggleLoading,
null, null,
mockedState, state,
[{ type: types.TOGGLE_MAIN_LOADING }], [{ type: types.TOGGLE_MAIN_LOADING }],
[], [],
done, done,
...@@ -106,25 +146,42 @@ describe('Actions Registry Store', () => { ...@@ -106,25 +146,42 @@ describe('Actions Registry Store', () => {
}); });
}); });
describe('deleteItem', () => { describe('deleteItem and multiDeleteItems', () => {
it('should perform DELETE request on destroyPath', done => { let deleted;
const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`; const destroyPath = `${TEST_HOST}/mygroup/myproject/container_registry/1.json`;
let deleted = false;
const expectDelete = done => {
expect(mock.history.delete.length).toBe(1);
expect(deleted).toBe(true);
done();
};
beforeEach(() => {
deleted = false;
mock.onDelete(destroyPath).replyOnce(() => { mock.onDelete(destroyPath).replyOnce(() => {
deleted = true; deleted = true;
return [200]; return [200];
}); });
});
it('deleteItem should perform DELETE request on destroyPath', done => {
testAction( testAction(
actions.deleteItem, actions.deleteItem,
{ {
destroyPath, destroyPath,
}, },
mockedState, state,
) )
.then(() => { .then(() => {
expect(mock.history.delete.length).toBe(1); expectDelete(done);
expect(deleted).toBe(true); })
done(); .catch(done.fail);
});
it('multiDeleteItems should perform DELETE request on path', done => {
testAction(actions.multiDeleteItems, { path: destroyPath, items: [1] }, state)
.then(() => {
expectDelete(done);
}) })
.catch(done.fail); .catch(done.fail);
}); });
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
const Component = Vue.extend(registry);
const props = {
endpoint: `${TEST_HOST}/foo`,
helpPagePath: 'foo',
noContainersImage: 'foo',
containersErrorImage: 'foo',
repositoryUrl: 'foo',
};
let vm;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('with data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
vm = mountComponent(Component, { ...props });
});
it('should render a list of repos', done => {
setTimeout(() => {
expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.container-image').length).toEqual(
reposServerResponse.length,
);
done();
});
}, 0);
});
describe('delete repository', () => {
it('should be possible to delete a repo', done => {
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
done();
});
}, 0);
});
});
describe('toggle repository', () => {
it('should open the container', done => {
setTimeout(() => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
).toContain('angle-up');
done();
});
});
}, 0);
});
});
});
describe('without data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { ...props });
});
it('should render empty message', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
done();
}, 0);
});
});
describe('while loading data', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, { ...props });
});
it('should render a loading spinner', done => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
done();
});
});
});
describe('invalid characters in path', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
vm = mountComponent(Component, {
...props,
characterError: true,
});
});
it('should render invalid characters error message', done => {
setTimeout(() => {
expect(vm.$el.querySelector('p')).not.toContain(
'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
);
done();
});
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores';
import * as types from '~/registry/stores/mutation_types';
import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
describe('collapsible registry container', () => {
let vm;
let mock;
const Component = Vue.extend(collapsibleComponent);
const findDeleteBtn = () => vm.$el.querySelector('.js-remove-repo');
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
store.commit(types.SET_REPOS_LIST, reposServerResponse);
vm = new Component({
store,
propsData: {
repo: repoPropsData,
},
}).$mount();
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.iconName).toEqual('angle-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')).not.toBeNull();
expect(vm.iconName).toEqual('angle-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();
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.iconName).toEqual('angle-right');
done();
});
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
expect(findDeleteBtn()).not.toBeNull();
});
it('should call deleteItem when confirming deletion', done => {
findDeleteBtn().click();
spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
document.querySelector(`#${vm.modalId} .btn-danger`).click();
expect(vm.deleteItem).toHaveBeenCalledWith(vm.repo);
done();
});
});
});
});
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import store from '~/registry/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { repoPropsData } from '../mock_data';
const [firstImage, secondImage] = repoPropsData.list;
describe('table registry', () => {
let vm;
const Component = Vue.extend(tableRegistry);
const bulkDeletePath = 'path';
const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry');
const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row');
const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input');
const findAllRowCheckboxes = () =>
Array.from(vm.$el.querySelectorAll('.js-select-checkbox input'));
const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`);
const createComponent = () => {
vm = mountComponentWithStore(Component, {
store,
props: {
repo: repoPropsData,
},
});
};
const selectAllCheckboxes = () => vm.selectAll();
const deselectAllCheckboxes = () => vm.deselectAll();
beforeEach(() => {
createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('rendering', () => {
it('should render a table with the registry list', () => {
expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
});
it('should render registry tag', () => {
const textRendered = vm.$el
.querySelector('.table tbody tr')
.textContent.trim()
// replace additional whitespace characters (e.g. new lines) with a single empty space
.replace(/\s\s+/g, ' ');
expect(textRendered).toContain(repoPropsData.list[0].tag);
expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
expect(textRendered).toContain(repoPropsData.list[0].layers);
expect(textRendered).toContain(repoPropsData.list[0].size);
});
});
describe('multi select', () => {
it('should support multiselect and selecting a row should enable delete button', done => {
findSelectAllCheckbox().click();
selectAllCheckboxes();
expect(findSelectAllCheckbox().checked).toBe(true);
Vue.nextTick(() => {
expect(findDeleteBtn().disabled).toBe(false);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
selectAllCheckboxes();
Vue.nextTick(() => {
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
expect(checkedValues.length).toBe(repoPropsData.list.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
selectAllCheckboxes();
deselectAllCheckboxes();
Vue.nextTick(() => {
const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
expect(checkedValues.length).toBe(0);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
selectAllCheckboxes();
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0, 1]);
expect(findDeleteBtn().disabled).toBe(false);
findDeleteBtn().click();
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
const modal = confirmationModal();
confirmationModal('.btn-danger').click();
expect(modal).toExist();
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([]);
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
});
});
describe('delete registry', () => {
beforeEach(() => {
vm.itemsToBeDeleted = [0];
});
it('should be possible to delete a registry', done => {
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0]);
expect(findDeleteBtn()).toBeDefined();
expect(findDeleteBtn().disabled).toBe(false);
expect(findDeleteBtnRow()).toBeDefined();
done();
});
});
it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => {
Vue.nextTick(() => {
expect(vm.itemsToBeDeleted).toEqual([0]);
expect(findDeleteBtn().disabled).toBe(false);
findDeleteBtn().click();
spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
Vue.nextTick(() => {
confirmationModal('.btn-danger').click();
expect(vm.itemsToBeDeleted).toEqual([]);
expect(vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
items: [firstImage.tag],
});
done();
});
});
});
});
describe('pagination', () => {
it('should be possible to change the page', () => {
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
});
});
describe('modal content', () => {
it('should show the singular title and image name when deleting a single image', done => {
findDeleteBtnRow().click();
Vue.nextTick(() => {
expect(vm.modalTitle).toBe('Remove image');
expect(vm.modalDescription).toContain(firstImage.tag);
done();
});
});
it('should show the plural title and image count when deleting more than one image', done => {
selectAllCheckboxes();
vm.setModalDescription();
Vue.nextTick(() => {
expect(vm.modalTitle).toBe('Remove images');
expect(vm.modalDescription).toContain('<b>2</b> images');
done();
});
});
});
});
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