Skip to content
Snippets Groups Projects
Commit 59a85c6b authored by Jacob Schatz's avatar Jacob Schatz
Browse files

Merge branch 'add-sidebar-specs' into 'master'

Add sidebar specs

See merge request !11132
parents 86e75ae0 f1d48c25
No related branches found
No related tags found
No related merge requests found
Showing
with 738 additions and 13 deletions
Loading
Loading
@@ -17,15 +17,21 @@ export default {
},
methods: {
listenForSlashCommands() {
$(document).on('ajax:success', '.gfm-form', (e, data) => {
const subscribedCommands = ['spend_time', 'time_estimate'];
const changedCommands = data.commands_changes
$(document).on('ajax:success', '.gfm-form', this.slashCommandListened);
},
slashCommandListened(e, data) {
const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands;
if (data !== undefined) {
changedCommands = data.commands_changes
? Object.keys(data.commands_changes)
: [];
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
this.mediator.fetch();
}
});
} else {
changedCommands = [];
}
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
this.mediator.fetch();
}
},
},
mounted() {
Loading
Loading
Loading
Loading
@@ -4,7 +4,7 @@ import sidebarAssignees from './components/assignees/sidebar_assignees';
 
import Mediator from './sidebar_mediator';
 
document.addEventListener('DOMContentLoaded', () => {
function domContentLoaded() {
const mediator = new Mediator(gl.sidebarOptions);
mediator.fetch();
 
Loading
Loading
@@ -17,5 +17,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
 
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
});
}
 
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
Loading
Loading
@@ -30,8 +30,8 @@ export default class SidebarMediator {
this.service.get()
.then((response) => {
const data = response.json();
this.store.processAssigneeData(data);
this.store.processTimeTrackingData(data);
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
})
.catch(() => new Flash('Error occured when fetching sidebar data'));
}
Loading
Loading
Loading
Loading
@@ -17,13 +17,13 @@ export default class SidebarStore {
return SidebarStore.singleton;
}
 
processAssigneeData(data) {
setAssigneeData(data) {
if (data.assignees) {
this.assignees = data.assignees;
}
}
 
processTimeTrackingData(data) {
setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
this.humanTimeEstimate = data.human_time_estimate;
Loading
Loading
export default {
createNumberRandomUsers(numberUsers) {
const users = [];
for (let i = 0; i < numberUsers; i = i += 1) {
users.push(
{
avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: (i + 1),
name: `GitLab User ${i}`,
username: `gitlab${i}`,
},
);
}
return users;
},
};
import Vue from 'vue';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title';
describe('AssigneeTitle component', () => {
let component;
let AssigneeTitleComponent;
beforeEach(() => {
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
});
describe('assignee title', () => {
it('renders assignee', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 1,
editable: false,
},
}).$mount();
expect(component.$el.innerText.trim()).toEqual('Assignee');
});
it('renders 2 assignees', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 2,
editable: false,
},
}).$mount();
expect(component.$el.innerText.trim()).toEqual('2 Assignees');
});
});
it('does not render spinner by default', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 0,
editable: false,
},
}).$mount();
expect(component.$el.querySelector('.fa')).toBeNull();
});
it('renders spinner when loading', () => {
component = new AssigneeTitleComponent({
propsData: {
loading: true,
numberOfAssignees: 0,
editable: false,
},
}).$mount();
expect(component.$el.querySelector('.fa')).not.toBeNull();
});
it('does not render edit link when not editable', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 0,
editable: false,
},
}).$mount();
expect(component.$el.querySelector('.edit-link')).toBeNull();
});
it('renders edit link when editable', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 0,
editable: true,
},
}).$mount();
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
});
});
import Vue from 'vue';
import Assignee from '~/sidebar/components/assignees/assignees';
import UsersMock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
describe('Assignee component', () => {
let component;
let AssigneeComponent;
beforeEach(() => {
AssigneeComponent = Vue.extend(Assignee);
});
describe('No assignees/users', () => {
it('displays no assignee icon when collapsed', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users: [],
editable: false,
},
}).$mount();
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
expect(collapsed.childElementCount).toEqual(1);
expect(collapsed.children[0].getAttribute('aria-label')).toEqual('No Assignee');
expect(collapsed.children[0].classList.contains('fa')).toEqual(true);
expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true);
});
it('displays only "No assignee" when no users are assigned and the issue is read-only', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users: [],
editable: false,
},
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
expect(componentTextNoUsers).toBe('No assignee');
expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1);
});
it('displays only "No assignee" when no users are assigned and the issue can be edited', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users: [],
editable: true,
},
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
expect(componentTextNoUsers.indexOf('No assignee')).toEqual(0);
expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0);
});
it('emits the assign-self event when "assign yourself" is clicked', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users: [],
editable: true,
},
}).$mount();
spyOn(component, '$emit');
component.$el.querySelector('.assign-yourself .btn-link').click();
expect(component.$emit).toHaveBeenCalledWith('assign-self');
});
});
describe('One assignee/user', () => {
it('displays one assignee icon when collapsed', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users: [
UsersMock.user,
],
editable: false,
},
}).$mount();
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
const assignee = collapsed.children[0];
expect(collapsed.childElementCount).toEqual(1);
expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`);
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
});
it('Shows one user with avatar, username and author name', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [
UsersMock.user,
],
editable: true,
},
}).$mount();
expect(component.$el.querySelector('.author_link')).not.toBeNull();
// The image
expect(component.$el.querySelector('.author_link img').getAttribute('src')).toEqual(UsersMock.user.avatar);
// Author name
expect(component.$el.querySelector('.author_link .author').innerText.trim()).toEqual(UsersMock.user.name);
// Username
expect(component.$el.querySelector('.author_link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`);
});
it('has the root url present in the assigneeUrl method', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [
UsersMock.user,
],
editable: true,
},
}).$mount();
expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1);
});
});
describe('Two or more assignees/users', () => {
it('displays two assignee icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: false,
},
}).$mount();
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
expect(collapsed.childElementCount).toEqual(2);
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`);
expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
});
it('displays one assignee icon and counter when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: false,
},
}).$mount();
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
expect(collapsed.childElementCount).toEqual(2);
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
expect(second.querySelector('.avatar-counter').innerText.trim()).toEqual('+2');
});
it('Shows two assignees', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
expect(component.$el.querySelectorAll('.user-item').length).toEqual(users.length);
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
it('Shows the "show-less" assignees label', (done) => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount);
expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - component.defaultRenderCount;
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
.not.toBe(`+${usersLabelExpectation} more`);
component.toggleShowLess();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
.toBe('- show less');
done();
});
});
it('Shows the "show-less" when "n+ more " label is clicked', (done) => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
component.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
.toBe('- show less');
done();
});
});
it('gets the count of avatar via a computed property ', () => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`);
});
describe('n+ more label', () => {
beforeEach(() => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
});
it('shows "+1 more" label', () => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
.toBe('+ 1 more');
});
it('shows "show less" label', (done) => {
component.toggleShowLess();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
.toBe('- show less');
done();
});
});
});
});
});
/* eslint-disable quote-props*/
const sidebarMockData = {
'GET': {
'/gitlab-org/gitlab-shell/issues/5.json': {
id: 45,
iid: 5,
author_id: 23,
description: 'Nulla ullam commodi delectus adipisci quis sit.',
lock_version: null,
milestone_id: 21,
position: 0,
state: 'closed',
title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.',
updated_by_id: 1,
created_at: '2017-02-02T21: 49: 49.664Z',
updated_at: '2017-05-03T22: 26: 03.760Z',
deleted_at: null,
time_estimate: 0,
total_time_spent: 0,
human_time_estimate: null,
human_total_time_spent: null,
branch_name: null,
confidential: false,
assignees: [
{
name: 'User 0',
username: 'user0',
id: 22,
state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0',
},
{
name: 'Marguerite Bartell',
username: 'tajuana',
id: 18,
state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana',
},
{
name: 'Laureen Ritchie',
username: 'michaele.will',
id: 16,
state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will',
},
],
due_date: null,
moved_to_id: null,
project_id: 4,
weight: null,
milestone: {
id: 21,
iid: 1,
project_id: 4,
title: 'v0.0',
description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.',
state: 'active',
created_at: '2017-02-02T21: 49: 30.530Z',
updated_at: '2017-02-02T21: 49: 30.530Z',
due_date: null,
start_date: null,
},
labels: [],
},
},
'PUT': {
'/gitlab-org/gitlab-shell/issues/5.json': {
data: {},
},
},
};
export default {
mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
editable: true,
currentUser: {
id: 1,
name: 'Administrator',
username: 'root',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
rootPath: '/',
},
time: {
time_estimate: 3600,
total_time_spent: 0,
human_time_estimate: '1h',
human_total_time_spent: null,
},
user: {
avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 1,
name: 'Administrator',
username: 'root',
},
sidebarMockInterceptor(request, next) {
const body = sidebarMockData[request.method.toUpperCase()][request.url];
next(request.respondWith(JSON.stringify(body), {
status: 200,
}));
},
};
import Vue from 'vue';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
describe('sidebar assignees', () => {
let component;
let SidebarAssigneeComponent;
preloadFixtures('issues/open-issue.html.raw');
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough();
spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough();
this.mediator = new SidebarMediator(Mock.mediator);
loadFixtures('issues/open-issue.html.raw');
this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
});
afterEach(() => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
});
it('calls the mediator when saves the assignees', () => {
component = new SidebarAssigneeComponent()
.$mount(this.sidebarAssigneesEl);
component.saveAssignees();
expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled();
});
it('calls the mediator when "assignSelf" method is called', () => {
component = new SidebarAssigneeComponent()
.$mount(this.sidebarAssigneesEl);
component.assignSelf();
expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled();
expect(this.mediator.store.assignees.length).toEqual(1);
});
});
import Vue from 'vue';
import SidebarBundleDomContentLoaded from '~/sidebar/sidebar_bundle';
import SidebarTimeTracking from '~/sidebar/components/time_tracking/sidebar_time_tracking';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
describe('sidebar bundle', () => {
gl.sidebarOptions = Mock.mediator;
beforeEach(() => {
spyOn(SidebarTimeTracking.methods, 'listenForSlashCommands').and.callFake(() => { });
preloadFixtures('issues/open-issue.html.raw');
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
loadFixtures('issues/open-issue.html.raw');
spyOn(Vue.prototype, '$mount');
SidebarBundleDomContentLoaded();
this.mediator = new SidebarMediator();
});
afterEach(() => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
});
it('the mediator should be already defined with some data', () => {
SidebarBundleDomContentLoaded();
expect(this.mediator.store).toBeDefined();
expect(this.mediator.service).toBeDefined();
expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
expect(this.mediator.store.rootPath).toEqual(Mock.mediator.rootPath);
expect(this.mediator.store.endPoint).toEqual(Mock.mediator.endPoint);
expect(this.mediator.store.editable).toEqual(Mock.mediator.editable);
});
it('the sidebar time tracking and assignees components to have been mounted', () => {
expect(Vue.prototype.$mount).toHaveBeenCalledTimes(2);
});
});
import Vue from 'vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
describe('Sidebar mediator', () => {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
});
afterEach(() => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
});
it('assigns yourself ', () => {
this.mediator.assignYourself();
expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
expect(this.mediator.store.assignees[0]).toEqual(Mock.mediator.currentUser);
});
it('saves assignees', (done) => {
this.mediator.saveAssignees('issue[assignee_ids]')
.then((resp) => {
expect(resp.status).toEqual(200);
done();
})
.catch(() => {});
});
it('fetches the data', () => {
spyOn(this.mediator.service, 'get').and.callThrough();
this.mediator.fetch();
expect(this.mediator.service.get).toHaveBeenCalled();
});
});
import Vue from 'vue';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
describe('Sidebar service', () => {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.service = new SidebarService('/gitlab-org/gitlab-shell/issues/5.json');
});
afterEach(() => {
SidebarService.singleton = null;
});
it('gets the data', (done) => {
this.service.get()
.then((resp) => {
expect(resp).toBeDefined();
done();
})
.catch(() => {});
});
it('updates the data', (done) => {
this.service.update('issue[assignee_ids]', [1])
.then((resp) => {
expect(resp).toBeDefined();
done();
})
.catch(() => {});
});
});
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
describe('Sidebar store', () => {
const assignee = {
id: 2,
name: 'gitlab user 2',
username: 'gitlab2',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
const anotherAssignee = {
id: 3,
name: 'gitlab user 3',
username: 'gitlab3',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
id: 1,
name: 'Administrator',
username: 'root',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
editable: true,
rootPath: '/',
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
});
});
afterEach(() => {
SidebarStore.singleton = null;
});
it('adds a new assignee', () => {
this.store.addAssignee(assignee);
expect(this.store.assignees.length).toEqual(1);
});
it('removes an assignee', () => {
this.store.removeAssignee(assignee);
expect(this.store.assignees.length).toEqual(0);
});
it('finds an existent assignee', () => {
let foundAssignee;
this.store.addAssignee(assignee);
foundAssignee = this.store.findAssignee(assignee);
expect(foundAssignee).toBeDefined();
expect(foundAssignee).toEqual(assignee);
foundAssignee = this.store.findAssignee(anotherAssignee);
expect(foundAssignee).toBeUndefined();
});
it('removes all assignees', () => {
this.store.removeAllAssignees();
expect(this.store.assignees.length).toEqual(0);
});
it('set assigned data', () => {
const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3),
};
this.store.setAssigneeData(users);
expect(this.store.assignees.length).toEqual(3);
});
it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
expect(this.store.totalTimeSpent).toEqual(Mock.time.total_time_spent);
expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate);
expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment