Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/javascripts')
-rw-r--r--spec/javascripts/api_spec.js34
-rw-r--r--spec/javascripts/boards/mock_data.js69
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js48
-rw-r--r--spec/javascripts/clusters/services/mock_data.js2
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js1
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js61
-rw-r--r--spec/javascripts/jobs/components/trigger_block_spec.js28
-rw-r--r--spec/javascripts/lib/utils/dom_utils_spec.js54
-rw-r--r--spec/javascripts/lib/utils/users_cache_spec.js110
-rw-r--r--spec/javascripts/notes/components/note_edited_text_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js3
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js1
-rw-r--r--spec/javascripts/notes/mock_data.js4
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js2
-rw-r--r--spec/javascripts/user_popovers_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js114
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js133
20 files changed, 974 insertions, 9 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 46f72214831..9d55c615450 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -333,6 +333,40 @@ describe('Api', () => {
});
});
+ describe('user', () => {
+ it('fetches single user', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
+ mock.onGet(expectedUrl).reply(200, {
+ name: 'testuser',
+ });
+
+ Api.user(userId)
+ .then(({ data }) => {
+ expect(data.name).toBe('testuser');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('user status', () => {
+ it('fetches single user status', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
+ mock.onGet(expectedUrl).reply(200, {
+ message: 'testmessage',
+ });
+
+ Api.userStatus(userId)
+ .then(({ data }) => {
+ expect(data.message).toBe('testmessage');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('commitPipelines', () => {
it('fetches pipelines for a given commit', done => {
const projectId = 'example/foobar';
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index c28e41ec175..14fff9223f4 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,5 +1,11 @@
import BoardService from '~/boards/services/board_service';
+export const boardObj = {
+ id: 1,
+ name: 'test',
+ milestone_id: null,
+};
+
export const listObj = {
id: 300,
position: 0,
@@ -40,6 +46,12 @@ export const BoardsMockData = {
},
],
},
+ '/test/issue-boards/milestones.json': [
+ {
+ id: 1,
+ title: 'test',
+ },
+ ],
},
POST: {
'/test/-/boards/1/lists': listObj,
@@ -70,3 +82,60 @@ export const mockBoardService = (opts = {}) => {
boardId,
});
};
+
+export const mockAssigneesList = [
+ {
+ id: 2,
+ name: 'Terrell Graham',
+ username: 'monserrate.gleichner',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/598fd02741ac58b88854a99d16704309?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/monserrate.gleichner',
+ path: '/monserrate.gleichner',
+ },
+ {
+ id: 12,
+ name: 'Susy Johnson',
+ username: 'tana_harvey',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e021a7b0f3e4ae53b5068d487e68c031?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/tana_harvey',
+ path: '/tana_harvey',
+ },
+ {
+ id: 20,
+ name: 'Conchita Eichmann',
+ username: 'juliana_gulgowski',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/c43c506cb6fd7b37017d3b54b94aa937?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/juliana_gulgowski',
+ path: '/juliana_gulgowski',
+ },
+ {
+ id: 6,
+ name: 'Bryce Turcotte',
+ username: 'melynda',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/cc2518f2c6f19f8fac49e1a5ee092a9b?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/melynda',
+ path: '/melynda',
+ },
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://127.0.0.1:3001/root',
+ path: '/root',
+ },
+];
+
+export const mockMilestone = {
+ id: 1,
+ state: 'active',
+ title: 'Milestone title',
+ description: 'Harum corporis aut consequatur quae dolorem error sequi quia.',
+ start_date: '2018-01-01',
+ due_date: '2019-12-31',
+};
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index e46edec9abb..14ef1193984 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -176,6 +176,54 @@ describe('Applications', () => {
});
});
+ describe('Cert-Manager application', () => {
+ describe('when not installed', () => {
+ it('renders email & allows editing', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'before@example.com',
+ status: 'installable',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('before@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toBe(null);
+ });
+ });
+
+ describe('when installed', () => {
+ it('renders email in readonly', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ cert_manager: {
+ title: 'Cert-Manager',
+ email: 'after@example.com',
+ status: 'installed',
+ },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-email').value).toEqual('after@example.com');
+ expect(vm.$el.querySelector('.js-email').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+ });
+
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 540d7f30858..3c3d9977ffb 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -42,6 +42,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
@@ -86,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ email: 'test@example.com',
},
],
},
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 7ea0878ad45..1ca55549094 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -115,6 +115,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[6].status_reason,
requestStatus: null,
requestReason: null,
+ email: mockResponseData.applications[6].email,
},
},
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 55ce19927e0..033b5e86dbe 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -26,7 +26,9 @@ import actions, {
toggleTreeOpen,
scrollToFile,
toggleShowTreeList,
+ renderFileForDiscussionId,
} from '~/diffs/store/actions';
+import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
import mockDiffFile from 'spec/diffs/mock_data/diff_file';
@@ -735,4 +737,63 @@ describe('DiffsStoreActions', () => {
expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true);
});
});
+
+ describe('renderFileForDiscussionId', () => {
+ const rootState = {
+ notes: {
+ discussions: [
+ {
+ id: '123',
+ diff_file: {
+ file_hash: 'HASH',
+ },
+ },
+ {
+ id: '456',
+ diff_file: {
+ file_hash: 'HASH',
+ },
+ },
+ ],
+ },
+ };
+ let commit;
+ let $emit;
+ let scrollToElement;
+ const state = ({ collapsed, renderIt }) => ({
+ diffFiles: [
+ {
+ file_hash: 'HASH',
+ collapsed,
+ renderIt,
+ },
+ ],
+ });
+
+ beforeEach(() => {
+ commit = jasmine.createSpy('commit');
+ scrollToElement = spyOnDependency(actions, 'scrollToElement').and.stub();
+ $emit = spyOn(eventHub, '$emit');
+ });
+
+ it('renders and expands file for the given discussion id', () => {
+ const localState = state({ collapsed: true, renderIt: false });
+
+ renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+
+ expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]);
+ expect($emit).toHaveBeenCalledTimes(1);
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ });
+
+ it('jumps to discussion on already rendered and expanded file', () => {
+ const localState = state({ collapsed: false, renderIt: true });
+
+ renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+
+ expect(commit).not.toHaveBeenCalled();
+ expect($emit).toHaveBeenCalledTimes(1);
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/jobs/components/trigger_block_spec.js b/spec/javascripts/jobs/components/trigger_block_spec.js
index 7254851a9e7..448197b82c0 100644
--- a/spec/javascripts/jobs/components/trigger_block_spec.js
+++ b/spec/javascripts/jobs/components/trigger_block_spec.js
@@ -31,8 +31,8 @@ describe('Trigger block', () => {
});
describe('with variables', () => {
- describe('reveal variables', () => {
- it('reveals variables on click', done => {
+ describe('hide/reveal variables', () => {
+ it('should toggle variables on click', done => {
vm = mountComponent(Component, {
trigger: {
short_token: 'bd7e',
@@ -48,6 +48,10 @@ describe('Trigger block', () => {
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Hide values',
+ );
+
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
'UPLOAD_TO_GCS',
);
@@ -58,6 +62,26 @@ describe('Trigger block', () => {
);
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true');
+
+ vm.$el.querySelector('.js-reveal-variables').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
+ 'Reveal values',
+ );
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_GCS',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
+
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
+ 'UPLOAD_TO_S3',
+ );
+
+ expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/lib/utils/dom_utils_spec.js b/spec/javascripts/lib/utils/dom_utils_spec.js
index 1fb2e4584a0..2bcf37f35c7 100644
--- a/spec/javascripts/lib/utils/dom_utils_spec.js
+++ b/spec/javascripts/lib/utils/dom_utils_spec.js
@@ -1,4 +1,6 @@
-import { addClassIfElementExists } from '~/lib/utils/dom_utils';
+import { addClassIfElementExists, canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+
+const TEST_MARGIN = 5;
describe('DOM Utils', () => {
describe('addClassIfElementExists', () => {
@@ -34,4 +36,54 @@ describe('DOM Utils', () => {
addClassIfElementExists(childElement, className);
});
});
+
+ describe('canScrollUp', () => {
+ [1, 100].forEach(scrollTop => {
+ it(`is true if scrollTop is > 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(true);
+ });
+ });
+
+ [0, -10].forEach(scrollTop => {
+ it(`is false if scrollTop is <= 0 (${scrollTop})`, () => {
+ expect(canScrollUp({ scrollTop })).toBe(false);
+ });
+ });
+
+ it('is true if scrollTop is > margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN + 1 }, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if scrollTop is <= margin', () => {
+ expect(canScrollUp({ scrollTop: TEST_MARGIN }, TEST_MARGIN)).toBe(false);
+ });
+ });
+
+ describe('canScrollDown', () => {
+ let element;
+
+ beforeEach(() => {
+ element = { scrollTop: 7, offsetHeight: 22, scrollHeight: 30 };
+ });
+
+ it('is true if element can be scrolled down', () => {
+ expect(canScrollDown(element)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down', () => {
+ element.scrollHeight -= 1;
+
+ expect(canScrollDown(element)).toBe(false);
+ });
+
+ it('is true if element can be scrolled down, with margin given', () => {
+ element.scrollHeight += TEST_MARGIN;
+
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(true);
+ });
+
+ it('is false if element cannot be scrolled down, with margin given', () => {
+ expect(canScrollDown(element, TEST_MARGIN)).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js
index 6adc19bdd51..acb5e024acd 100644
--- a/spec/javascripts/lib/utils/users_cache_spec.js
+++ b/spec/javascripts/lib/utils/users_cache_spec.js
@@ -3,7 +3,9 @@ import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
const dummyUsername = 'win';
- const dummyUser = 'has a farm';
+ const dummyUserId = 123;
+ const dummyUser = { name: 'has a farm', username: 'farmer' };
+ const dummyUserStatus = 'my status';
beforeEach(() => {
UsersCache.internalStorage = {};
@@ -135,4 +137,110 @@ describe('UsersCache', () => {
.catch(done.fail);
});
});
+
+ describe('retrieveById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'user').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUser,
+ });
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = dummyUser;
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('retrieveStatusById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'userStatus').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUserStatus,
+ });
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = { status: dummyUserStatus };
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js
index e0b991c32ec..e4c8d954d50 100644
--- a/spec/javascripts/notes/components/note_edited_text_spec.js
+++ b/spec/javascripts/notes/components/note_edited_text_spec.js
@@ -39,7 +39,7 @@ describe('note_edited_text', () => {
});
it('should render provided user information', () => {
- const authorLink = vm.$el.querySelector('.js-vue-author');
+ const authorLink = vm.$el.querySelector('.js-user-link');
expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path);
expect(authorLink.textContent.trim()).toEqual(props.editedBy.name);
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 379780f43a0..6d1a7ef370f 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -42,6 +42,9 @@ describe('note_header component', () => {
it('should render user information', () => {
expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root');
expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root');
+ expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link');
});
it('should render timestamp link', () => {
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index ab9c52346d6..e4d29a3860c 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -83,6 +83,7 @@ describe('noteable_discussion component', () => {
it('expands next unresolved discussion', done => {
const discussion2 = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
discussion2.resolved = false;
+ discussion2.active = true;
discussion2.id = 'next'; // prepare this for being identified as next one (to be jumped to)
vm.$store.dispatch('setInitialNotes', [discussionMock, discussion2]);
window.mrTabs.currentAction = 'show';
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index ad0e793b915..7ae45c40c28 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -305,6 +305,7 @@ export const discussionMock = {
],
individual_note: false,
resolvable: true,
+ active: true,
};
export const loggedOutnoteableData = {
@@ -1173,6 +1174,7 @@ export const discussion1 = {
id: 'abc1',
resolvable: true,
resolved: false,
+ active: true,
diff_file: {
file_path: 'about.md',
},
@@ -1209,6 +1211,7 @@ export const discussion2 = {
id: 'abc2',
resolvable: true,
resolved: false,
+ active: true,
diff_file: {
file_path: 'README.md',
},
@@ -1226,6 +1229,7 @@ export const discussion2 = {
export const discussion3 = {
id: 'abc3',
resolvable: true,
+ active: true,
resolved: false,
diff_file: {
file_path: 'README.md',
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 24c2b3e6570..2e3cd5e8f36 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -124,7 +124,7 @@ describe('Actions Notes Store', () => {
{ discussionId: discussionMock.id },
{ notes: [discussionMock] },
[{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }],
- [],
+ [{ type: 'diffs/renderFileForDiscussionId', payload: discussionMock.id }],
done,
);
});
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
new file mode 100644
index 00000000000..6cf8dd81b36
--- /dev/null
+++ b/spec/javascripts/user_popovers_spec.js
@@ -0,0 +1,66 @@
+import initUserPopovers from '~/user_popovers';
+import UsersCache from '~/lib/utils/users_cache';
+
+describe('User Popovers', () => {
+ const selector = '.js-user-link';
+
+ const dummyUser = { name: 'root' };
+ const dummyUserStatus = { message: 'active' };
+
+ const triggerEvent = (eventName, el) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(eventName, true, true, window);
+
+ el.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" data-username="root" data-original-title="" title="">
+ Root
+ </a>
+ `);
+
+ const usersCacheSpy = () => Promise.resolve(dummyUser);
+ spyOn(UsersCache, 'retrieveById').and.callFake(userId => usersCacheSpy(userId));
+
+ const userStatusCacheSpy = () => Promise.resolve(dummyUserStatus);
+ spyOn(UsersCache, 'retrieveStatusById').and.callFake(userId => userStatusCacheSpy(userId));
+
+ initUserPopovers(document.querySelectorAll('.js-user-link'));
+ });
+
+ it('Should Show+Hide Popover on mouseenter and mouseleave', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ const shownPopover = document.querySelector('.popover');
+
+ expect(shownPopover).not.toBeNull();
+
+ expect(shownPopover.innerHTML).toContain(dummyUser.name);
+ expect(UsersCache.retrieveById).toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ setTimeout(() => {
+ // After Mouse leave it should be hidden now
+ expect(document.querySelector('.popover')).toBeNull();
+ done();
+ });
+ }, 210); // We need to wait until the 200ms mouseover delay is over, only then the popover will be visible
+ });
+
+ it('Should Not show a popover on short mouse over', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ expect(document.querySelector('.popover')).toBeNull();
+ expect(UsersCache.retrieveById).not.toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
new file mode 100644
index 00000000000..9eac75fac96
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+
+import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockAssigneesList } from 'spec/boards/mock_data';
+
+const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
+ const Component = Vue.extend(IssueAssignees);
+
+ return mountComponent(Component, {
+ assignees,
+ cssClass,
+ });
+};
+
+describe('IssueAssigneesComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('data', () => {
+ it('returns default data props', () => {
+ expect(vm.maxVisibleAssignees).toBe(2);
+ expect(vm.maxAssigneeAvatars).toBe(3);
+ expect(vm.maxAssignees).toBe(99);
+ });
+ });
+
+ describe('computed', () => {
+ describe('countOverLimit', () => {
+ it('should return difference between assignees count and maxVisibleAssignees', () => {
+ expect(vm.countOverLimit).toBe(mockAssigneesList.length - vm.maxVisibleAssignees);
+ });
+ });
+
+ describe('assigneesToShow', () => {
+ it('should return assignees containing only 2 items when count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneesToShow.length).toBe(2);
+ });
+
+ it('should return all assignees as it is when count less than maxAssigneeAvatars', () => {
+ vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
+
+ expect(vm.assigneesToShow.length).toBe(3);
+ });
+ });
+
+ describe('assigneesCounterTooltip', () => {
+ it('should return string containing count of remaining assignees when count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneesCounterTooltip).toBe('3 more assignees');
+ });
+ });
+
+ describe('shouldRenderAssigneesCounter', () => {
+ it('should return `false` when assignees count less than maxAssigneeAvatars', () => {
+ vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
+
+ expect(vm.shouldRenderAssigneesCounter).toBe(false);
+ });
+
+ it('should return `true` when assignees count more than maxAssigneeAvatars', () => {
+ expect(vm.shouldRenderAssigneesCounter).toBe(true);
+ });
+ });
+
+ describe('assigneeCounterLabel', () => {
+ it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneeCounterLabel).toBe('+3');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('avatarUrlTitle', () => {
+ it('returns string containing alt text for assignee avatar', () => {
+ expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-assignees`', () => {
+ expect(vm.$el.classList.contains('issue-assignees')).toBe(true);
+ });
+
+ it('renders assignee avatars', () => {
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2);
+ });
+
+ it('renders assignee tooltips', () => {
+ const tooltipText = vm.$el
+ .querySelectorAll('.user-avatar-link')[0]
+ .querySelector('.js-assignee-tooltip').innerText;
+
+ expect(tooltipText).toContain('Assignee');
+ expect(tooltipText).toContain('Terrell Graham');
+ expect(tooltipText).toContain('@monserrate.gleichner');
+ });
+
+ it('renders additional assignees count', () => {
+ const avatarCounterEl = vm.$el.querySelector('.avatar-counter');
+
+ expect(avatarCounterEl.innerText.trim()).toBe('+3');
+ expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..8fca2637326
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,234 @@
+import Vue from 'vue';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockMilestone } from 'spec/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mountComponent(Component, {
+ milestone,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', done => {
+ const vmStartUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStartUndefined.isMilestoneStarted).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStartUndefined.$destroy();
+ });
+
+ it('should return `true` when milestone start date is past current date', done => {
+ const vmStarted = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarted.isMilestoneStarted).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarted.$destroy();
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', done => {
+ const vmDueUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDueUndefined.isMilestonePastDue).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDueUndefined.$destroy();
+ });
+
+ it('should return `true` when milestone due is past current date', done => {
+ const vmPastDue = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmPastDue.isMilestonePastDue).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmPastDue.$destroy();
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', done => {
+ const vmDueUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDueUndefined.$destroy();
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', done => {
+ const vmDatesUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDatesUndefined.$destroy();
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', done => {
+ const vmFuture = createComponent(
+ Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmFuture.$destroy();
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', done => {
+ const vmStarted = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarted.milestoneDatesHuman).toContain('Started');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarted.$destroy();
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
+ const vmStarts = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmStarts.milestoneDatesHuman).toContain('Starts');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmStarts.$destroy();
+ });
+
+ it('returns empty string when milestone start and due dates are not present', done => {
+ const vmDatesUndefined = createComponent(
+ Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+
+ vmDatesUndefined.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 5c4aa7cf844..c5045afc5b0 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import defaultAvatarUrl from '~/../images/no_avatar.png';
const DEFAULT_PROPS = {
size: 99,
@@ -76,6 +77,18 @@ describe('User Avatar Image Component', function() {
});
});
+ describe('Initialization without src', function() {
+ beforeEach(function() {
+ vm = mountComponent(UserAvatarImage);
+ });
+
+ it('should have default avatar image', function() {
+ const imageElement = vm.$el.querySelector('img');
+
+ expect(imageElement.getAttribute('src')).toBe(defaultAvatarUrl);
+ });
+ });
+
describe('dynamic tooltip content', () => {
const props = DEFAULT_PROPS;
const slots = {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 0151ad23ba2..f2472fd377c 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -74,9 +74,7 @@ describe('User Avatar Link Component', function() {
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(
- this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
- ).toEqual('');
+ expect(this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip')).toBeNull();
});
it('should render username prop in <span>', function() {
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
new file mode 100644
index 00000000000..1578b0f81f9
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -0,0 +1,133 @@
+import Vue from 'vue';
+import userPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const DEFAULT_PROPS = {
+ loaded: true,
+ user: {
+ username: 'root',
+ name: 'Administrator',
+ location: 'Vienna',
+ bio: null,
+ organization: null,
+ status: null,
+ },
+};
+
+const UserPopover = Vue.extend(userPopover);
+
+describe('User Popover Component', () => {
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" title="testuser">
+ Root
+ </a>
+ `);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Empty', () => {
+ beforeEach(() => {
+ vm = mountComponent(UserPopover, {
+ target: document.querySelector('.js-user-link'),
+ user: {
+ name: null,
+ username: null,
+ location: null,
+ bio: null,
+ organization: null,
+ status: null,
+ },
+ });
+ });
+
+ it('should return skeleton loaders', () => {
+ expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4);
+ });
+ });
+
+ describe('basic data', () => {
+ it('should show basic fields', () => {
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location);
+ });
+ });
+
+ describe('job data', () => {
+ it('should show only bio if no organization is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer');
+ });
+
+ it('should show only organization if no bio is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('GitLab');
+ });
+
+ it('should have full job line when we have bio and organization', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer at GitLab');
+ });
+ });
+
+ describe('status data', () => {
+ it('should show only message', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ });
+
+ it('should show message and emoji', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { emoji: 'basketball_player', message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ status: { emoji: 'basketball_player', message: 'Hello World' },
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"');
+ });
+ });
+});