Commit 59a85c6b authored by Jacob Schatz's avatar Jacob Schatz

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

Add sidebar specs

See merge request !11132
parents 86e75ae0 f1d48c25
......@@ -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() {
......
......@@ -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();
......@@ -17,5 +17,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
});
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
......@@ -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'));
}
......
......@@ -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;
......
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);
});
});
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