diff options
Diffstat (limited to 'spec/javascripts')
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"'); + }); + }); +}); |