diff options
author | Grzegorz Bizon <grzegorz@gitlab.com> | 2018-12-20 14:47:01 +0300 |
---|---|---|
committer | Grzegorz Bizon <grzegorz@gitlab.com> | 2018-12-20 14:47:01 +0300 |
commit | c111e2657df22c811191135369d599923dc89f54 (patch) | |
tree | 2de468666124191dcf815cf4dd92ea21fa76ca16 /spec/javascripts | |
parent | cad0661aadff50b4d2c2b4cc7b012809b945213c (diff) | |
parent | 37c934e089508e053e6ad4cf075b00cfaab53f3c (diff) |
Merge branch 'master' into 'feature/option-to-make-variables-protected'
Conflicts:
db/schema.rb
Diffstat (limited to 'spec/javascripts')
81 files changed, 3443 insertions, 784 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 091edf13cfe..9d55c615450 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -123,7 +123,7 @@ describe('Api', () => { }); }); - describe('mergerequest', () => { + describe('projectMergeRequest', () => { it('fetches a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -132,7 +132,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequest(projectPath, mergeRequestId) + Api.projectMergeRequest(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -141,7 +141,7 @@ describe('Api', () => { }); }); - describe('mergerequest changes', () => { + describe('projectMergeRequestChanges', () => { it('fetches the changes of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -150,7 +150,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequestChanges(projectPath, mergeRequestId) + Api.projectMergeRequestChanges(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -159,7 +159,7 @@ describe('Api', () => { }); }); - describe('mergerequest versions', () => { + describe('projectMergeRequestVersions', () => { it('fetches the versions of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -170,7 +170,7 @@ describe('Api', () => { }, ]); - Api.mergeRequestVersions(projectPath, mergeRequestId) + Api.projectMergeRequestVersions(projectPath, mergeRequestId) .then(({ data }) => { expect(data.length).toBe(1); expect(data[0].id).toBe(123); @@ -180,6 +180,23 @@ describe('Api', () => { }); }); + describe('projectRunners', () => { + it('fetches the runners of a project', done => { + const projectPath = 7; + const params = { scope: 'active' }; + const mockData = [{ id: 4 }]; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`; + mock.onGet(expectedUrl, { params }).reply(200, mockData); + + Api.projectRunners(projectPath, { params }) + .then(({ data }) => { + expect(data).toEqual(mockData); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('newLabel', () => { it('creates a new label', done => { const namespace = 'some namespace'; @@ -316,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/blob_edit/blob_bundle_spec.js b/spec/javascripts/blob_edit/blob_bundle_spec.js index 759d170af77..57f60a4a3dd 100644 --- a/spec/javascripts/blob_edit/blob_bundle_spec.js +++ b/spec/javascripts/blob_edit/blob_bundle_spec.js @@ -14,6 +14,7 @@ describe('EditBlob', () => { setFixtures(` <div class="js-edit-blob-form"> <button class="js-commit-button"></button> + <a class="btn btn-cancel" href="#"></a> </div>`); blobBundle(); }); @@ -27,4 +28,10 @@ describe('EditBlob', () => { expect(window.onbeforeunload).toBeNull(); }); + + it('removes beforeunload listener when cancel link is clicked', () => { + $('.btn.btn-cancel').click(); + + expect(window.onbeforeunload).toBeNull(); + }); }); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 54f1edfb1f9..22f192bc7f3 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -65,6 +65,13 @@ describe('Store', () => { expect(list).toBeDefined(); }); + it('finds list by label ID', () => { + boardsStore.addList(listObj); + const list = boardsStore.findListByLabelId(listObj.label.id); + + expect(list.id).toBe(listObj.id); + }); + it('gets issue when new list added', done => { boardsStore.addList(listObj); const list = boardsStore.findList('id', listObj.id); diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js index 9e49330c052..054cf8c5b7d 100644 --- a/spec/javascripts/boards/components/issue_due_date_spec.js +++ b/spec/javascripts/boards/components/issue_due_date_spec.js @@ -49,10 +49,11 @@ describe('Issue Due Date component', () => { it('should render month and day for other dates', () => { date.setDate(date.getDate() + 17); vm = createComponent(date); + const today = new Date(); + const isDueInCurrentYear = today.getFullYear() === date.getFullYear(); + const format = isDueInCurrentYear ? 'mmm d' : 'mmm d, yyyy'; - expect(vm.$el.querySelector('time').textContent.trim()).toEqual( - dateFormat(date, 'mmm d', true), - ); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, format, true)); }); it('should contain the correct `.text-danger` css class for overdue issue', () => { diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 437ab4bb3df..54fb0e8228b 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -55,15 +55,27 @@ describe('Issue model', () => { expect(issue.labels.length).toBe(2); }); - it('does not add existing label', () => { + it('does not add label if label id exists', () => { + issue.addLabel({ + id: 1, + title: 'test 2', + color: 'blue', + description: 'testing', + }); + + expect(issue.labels.length).toBe(1); + expect(issue.labels[0].color).toBe('red'); + }); + + it('adds other label with same title', () => { issue.addLabel({ id: 2, title: 'test', color: 'blue', - description: 'bugs!', + description: 'other test', }); - expect(issue.labels.length).toBe(1); + expect(issue.labels.length).toBe(2); }); it('finds label', () => { 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 928bf70f3a2..14ef1193984 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import applications from '~/clusters/components/applications.vue'; +import { CLUSTER_TYPE } from '~/clusters/constants'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Applications', () => { @@ -14,9 +15,10 @@ describe('Applications', () => { vm.$destroy(); }); - describe('', () => { + describe('Project cluster applications', () => { beforeEach(() => { vm = mountComponent(Applications, { + type: CLUSTER_TYPE.PROJECT, applications: { helm: { title: 'Helm Tiller' }, ingress: { title: 'Ingress' }, @@ -30,31 +32,76 @@ describe('Applications', () => { }); it('renders a row for Helm Tiller', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined(); + expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull(); }); it('renders a row for Ingress', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined(); + expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull(); }); it('renders a row for Cert-Manager', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).toBeDefined(); + expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull(); }); it('renders a row for Prometheus', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined(); + expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull(); }); it('renders a row for GitLab Runner', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); + expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull(); }); it('renders a row for Jupyter', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null); + expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull(); }); it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null); + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); + }); + }); + + describe('Group cluster applications', () => { + beforeEach(() => { + vm = mountComponent(Applications, { + type: CLUSTER_TYPE.GROUP, + applications: { + helm: { title: 'Helm Tiller' }, + ingress: { title: 'Ingress' }, + cert_manager: { title: 'Cert-Manager' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub' }, + knative: { title: 'Knative' }, + }, + }); + }); + + it('renders a row for Helm Tiller', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull(); + }); + + it('renders a row for Ingress', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull(); + }); + + it('renders a row for Cert-Manager', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull(); + }); + + it('renders a row for Prometheus', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull(); + }); + + it('renders a row for GitLab Runner', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull(); + }); + + it('renders a row for Jupyter', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull(); + }); + + it('renders a row for Knative', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); }); }); @@ -129,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/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 1a0b7612ee9..a2cbc0f3c72 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -1,31 +1,44 @@ -import Vue from 'vue'; -import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import { TEST_HOST } from 'spec/test_constants'; import App from '~/diffs/components/app.vue'; +import NoChanges from '~/diffs/components/no_changes.vue'; +import DiffFile from '~/diffs/components/diff_file.vue'; import createDiffsStore from '../create_diffs_store'; describe('diffs/components/app', () => { const oldMrTabs = window.mrTabs; - const Component = Vue.extend(App); - + let store; let vm; - beforeEach(() => { - // setup globals (needed for component to mount :/) - window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); + function createComponent(props = {}, extendStore = () => {}) { + const localVue = createLocalVue(); - // setup component - const store = createDiffsStore(); + localVue.use(Vuex); + + store = createDiffsStore(); store.state.diffs.isLoading = false; - vm = mountComponentWithStore(Component, { - store, - props: { + extendStore(store); + + vm = shallowMount(localVue.extend(App), { + localVue, + propsData: { endpoint: `${TEST_HOST}/diff/endpoint`, projectPath: 'namespace/project', currentUser: {}, + changesEmptyStateIllustration: '', + ...props, }, + store, }); + } + + beforeEach(() => { + // setup globals (needed for component to mount :/) + window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); + window.mrTabs.expandViewContainer = jasmine.createSpy(); + window.location.hash = 'ABC_123'; }); afterEach(() => { @@ -33,10 +46,53 @@ describe('diffs/components/app', () => { window.mrTabs = oldMrTabs; // reset component - vm.$destroy(); + vm.destroy(); }); it('does not show commit info', () => { - expect(vm.$el).not.toContainElement('.blob-commit-info'); + createComponent(); + + expect(vm.contains('.blob-commit-info')).toBe(false); + }); + + it('sets highlighted row if hash exists in location object', done => { + createComponent({ + shouldShow: true, + }); + + // Component uses $nextTick so we wait until that has finished + setTimeout(() => { + expect(store.state.diffs.highlightedRow).toBe('ABC_123'); + + done(); + }); + }); + + describe('empty state', () => { + it('renders empty state when no diff files exist', () => { + createComponent(); + + expect(vm.contains(NoChanges)).toBe(true); + }); + + it('does not render empty state when diff files exist', () => { + createComponent({}, () => { + store.state.diffs.diffFiles.push({ + id: 1, + }); + }); + + expect(vm.contains(NoChanges)).toBe(false); + expect(vm.findAll(DiffFile).length).toBe(1); + }); + + it('does not render empty state when versions match', () => { + createComponent({}, () => { + store.state.diffs.startVersion = { version_index: 1 }; + store.state.diffs.mergeRequestDiff = { version_index: 1 }; + }); + + expect(vm.contains(NoChanges)).toBe(false); + }); }); }); diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index c25f6167163..9e158327a77 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -17,6 +17,7 @@ describe('DiffContent', () => { current_user: { can_create_note: false, }, + preview_note_path: 'path/to/preview', }; vm = mountComponentWithStore(Component, { @@ -49,6 +50,45 @@ describe('DiffContent', () => { }); }); + describe('empty files', () => { + beforeEach(() => { + vm.diffFile.empty = true; + vm.diffFile.highlighted_diff_lines = []; + vm.diffFile.parallel_diff_lines = []; + }); + + it('should render a message', done => { + vm.$nextTick(() => { + const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); + + expect(block).not.toBe(null); + expect(block.textContent.trim()).toContain('Empty file'); + + done(); + }); + }); + + it('should not render multiple messages', done => { + vm.diffFile.mode_changed = true; + vm.diffFile.b_mode = '100755'; + vm.diffFile.viewer.name = 'mode_changed'; + + vm.$nextTick(() => { + expect(vm.$el.querySelectorAll('.nothing-here-block').length).toBe(1); + + done(); + }); + }); + + it('should not render diff table', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('table')).toBe(null); + + done(); + }); + }); + }); + describe('Non-Text diffs', () => { beforeEach(() => { vm.diffFile.viewer.name = 'image'; diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 9530b50c729..b77907ff26f 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -464,7 +464,11 @@ describe('diff_file_header', () => { propsCopy.addMergeRequestButtons = true; propsCopy.diffFile.deleted_file = true; - const discussionGetter = () => [diffDiscussionMock]; + const discussionGetter = () => [ + { + ...diffDiscussionMock, + }, + ]; const notesModuleMock = notesModule(); notesModuleMock.getters.discussions = discussionGetter; vm = mountComponentWithStore(Component, { diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 51bb4807960..1af49282c36 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -74,6 +74,32 @@ describe('DiffFile', () => { }); }); + it('should be collapsed for renamed files', done => { + vm.file.renderIt = true; + vm.file.collapsed = false; + vm.file.highlighted_diff_lines = null; + vm.file.renamed_file = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + + done(); + }); + }); + + it('should be collapsed for mode changed files', done => { + vm.file.renderIt = true; + vm.file.collapsed = false; + vm.file.highlighted_diff_lines = null; + vm.file.mode_changed = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).not.toContain('This diff is collapsed'); + + done(); + }); + }); + it('should have loading icon while loading a collapsed diffs', done => { vm.file.collapsed = true; vm.isLoadingCollapsedDiff = true; diff --git a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js index ad2605a5c5c..cdd30919b09 100644 --- a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js +++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js @@ -89,6 +89,35 @@ describe('DiffGutterAvatars', () => { expect(component.discussions[0].expanded).toEqual(false); component.$store.dispatch('setInitialNotes', []); }); + + it('forces expansion of all discussions', () => { + spyOn(component.$store, 'dispatch'); + + component.discussions[0].expanded = true; + component.discussions.push({ + ...component.discussions[0], + id: '123test', + expanded: false, + }); + + component.toggleDiscussions(); + + expect(component.$store.dispatch.calls.argsFor(0)).toEqual([ + 'toggleDiscussion', + { + discussionId: component.discussions[0].id, + forceExpanded: true, + }, + ]); + + expect(component.$store.dispatch.calls.argsFor(1)).toEqual([ + 'toggleDiscussion', + { + discussionId: component.discussions[1].id, + forceExpanded: true, + }, + ]); + }); }); }); diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index 81b66cf7c9b..b983dc35a57 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -62,6 +62,7 @@ describe('DiffLineNoteForm', () => { component.$nextTick(() => { expect(component.cancelCommentForm).toHaveBeenCalledWith({ lineCode: diffLines[0].line_code, + fileHash: component.diffFileHash, }); expect(component.resetAutoSave).toHaveBeenCalled(); diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js new file mode 100644 index 00000000000..170e661beea --- /dev/null +++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import store from '~/mr_notes/stores'; +import DiffTableCell from '~/diffs/components/diff_table_cell.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('DiffTableCell', () => { + const createComponent = options => + createComponentWithStore(Vue.extend(DiffTableCell), store, { + line: diffFileMockData.highlighted_diff_lines[0], + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + ...options, + }).$mount(); + + it('does not highlight row when isHighlighted prop is false', done => { + const vm = createComponent({ isHighlighted: false }); + + vm.$nextTick() + .then(() => { + expect(vm.$el.classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('highlights row when isHighlighted prop is true', done => { + const vm = createComponent({ isHighlighted: true }); + + vm.$nextTick() + .then(() => { + expect(vm.$el.classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js new file mode 100644 index 00000000000..97926f6625e --- /dev/null +++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import store from '~/mr_notes/stores'; +import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('InlineDiffTableRow', () => { + let vm; + const thisLine = diffFileMockData.highlighted_diff_lines[0]; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not add hll class to line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = thisLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/javascripts/diffs/components/no_changes_spec.js index 7237274eb43..e45d34bf9d5 100644 --- a/spec/javascripts/diffs/components/no_changes_spec.js +++ b/spec/javascripts/diffs/components/no_changes_spec.js @@ -1 +1,40 @@ -// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { createStore } from '~/mr_notes/stores'; +import NoChanges from '~/diffs/components/no_changes.vue'; + +describe('Diff no changes empty state', () => { + let vm; + + function createComponent(extendStore = () => {}) { + const localVue = createLocalVue(); + localVue.use(Vuex); + + const store = createStore(); + extendStore(store); + + vm = shallowMount(localVue.extend(NoChanges), { + localVue, + store, + propsData: { + changesEmptyStateIllustration: '', + }, + }); + } + + afterEach(() => { + vm.destroy(); + }); + + it('prevents XSS', () => { + createComponent(store => { + // eslint-disable-next-line no-param-reassign + store.state.notes.noteableData = { + source_branch: '<script>alert("test");</script>', + target_branch: '<script>alert("test");</script>', + }; + }); + + expect(vm.contains('script')).toBe(false); + }); +}); diff --git a/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js new file mode 100644 index 00000000000..311eaaaa7c8 --- /dev/null +++ b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import { createStore } from '~/mr_notes/stores'; +import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffFileMockData from '../mock_data/diff_file'; + +describe('ParallelDiffTableRow', () => { + describe('when one side is empty', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[0]; + const rightLine = diffFileMockData.parallel_diff_lines[0].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight non empty line content when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('highlights nonempty line content when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when both sides have content', () => { + let vm; + const thisLine = diffFileMockData.parallel_diff_lines[2]; + const rightLine = diffFileMockData.parallel_diff_lines[2].right; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { + line: thisLine, + fileHash: diffFileMockData.file_hash, + contextLinesPath: 'contextLinesPath', + isHighlighted: false, + }).$mount(); + }); + + it('does not highlight either line when line does not match highlighted row', done => { + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + + it('adds hll class to lineContent when line is the highlighted row', done => { + vm.$nextTick() + .then(() => { + vm.$store.state.diffs.highlightedRow = rightLine.line_code; + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); + expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index 5ffe5a366ba..c1e9f791925 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -487,10 +487,19 @@ export default { ], }, diff_discussion: true, - truncated_diff_lines: - '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', - image_diff_html: - '<div class="image js-replaced-image" data="">\n<div class="two-up view">\n<div class="wrap">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n<div class="wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<p class="image-info hide">\n<span class="meta-filesize">22.3 KB</span>\n|\n<strong>W:</strong>\n<span class="meta-width"></span>\n|\n<strong>H:</strong>\n<span class="meta-height"></span>\n</p>\n</div>\n</div>\n<div class="swipe view hide">\n<div class="swipe-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="swipe-wrap">\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n</div>\n<span class="swipe-bar">\n<span class="top-handle"></span>\n<span class="bottom-handle"></span>\n</span>\n</div>\n</div>\n<div class="onion-skin view hide">\n<div class="onion-skin-frame">\n<div class="frame deleted">\n<img alt="CHANGELOG" src="http://localhost:3000/gitlab-org/gitlab-test/raw/e63f41fe459e62e1228fcef60d7189127aeba95a/CHANGELOG" />\n</div>\n<div class="added frame js-image-frame" data-note-type="DiffNote" data-position="{"base_sha":"e63f41fe459e62e1228fcef60d7189127aeba95a","start_sha":"d9eaefe5a676b820c57ff18cf5b68316025f7962","head_sha":"c48ee0d1bf3b30453f5b32250ce03134beaa6d13","old_path":"CHANGELOG","new_path":"CHANGELOG","position_type":"text","old_line":null,"new_line":2}">\n<img alt="CHANGELOG" draggable="false" src="http://localhost:3000/gitlab-org/gitlab-test/raw/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG" />\n</div>\n\n<div class="controls">\n<div class="transparent"></div>\n<div class="drag-track">\n<div class="dragger" style="left: 0px;"></div>\n</div>\n<div class="opaque"></div>\n</div>\n</div>\n</div>\n</div>\n<div class="view-modes hide">\n<ul class="view-modes-menu">\n<li class="two-up" data-mode="two-up">2-up</li>\n<li class="swipe" data-mode="swipe">Swipe</li>\n<li class="onion-skin" data-mode="onion-skin">Onion skin</li>\n</ul>\n</div>\n', + truncated_diff_lines: [ + { + text: 'line', + rich_text: + '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', + can_receive_suggestion: true, + line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1', + type: 'new', + old_line: null, + new_line: 1, + meta_data: null, + }, + ], }; export const imageDiffDiscussions = [ diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index acd95a3dd8b..033b5e86dbe 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -22,12 +22,16 @@ import actions, { expandAllFiles, toggleFileDiscussions, saveDiffDiscussion, + setHighlightedRow, 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'; import testAction from '../../helpers/vuex_action_helper'; describe('DiffsStoreActions', () => { @@ -92,6 +96,14 @@ describe('DiffsStoreActions', () => { }); }); + describe('setHighlightedRow', () => { + it('should set lineHash and fileHash of highlightedRow', () => { + testAction(setHighlightedRow, 'ABC_123', {}, [ + { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' }, + ]); + }); + }); + describe('assignDiscussionsToDiff', () => { it('should merge discussions into diffs', done => { const state = { @@ -310,13 +322,13 @@ describe('DiffsStoreActions', () => { describe('showCommentForm', () => { it('should call mutation to show comment form', done => { - const payload = { lineCode: 'lineCode' }; + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; testAction( showCommentForm, payload, {}, - [{ type: types.ADD_COMMENT_FORM_LINE, payload }], + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: true } }], [], done, ); @@ -325,13 +337,13 @@ describe('DiffsStoreActions', () => { describe('cancelCommentForm', () => { it('should call mutation to cancel comment form', done => { - const payload = { lineCode: 'lineCode' }; + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; testAction( cancelCommentForm, payload, {}, - [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: false } }], [], done, ); @@ -370,27 +382,50 @@ describe('DiffsStoreActions', () => { describe('loadCollapsedDiff', () => { it('should fetch data and call mutation with response and the give parameter', done => { - const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' }; + const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' }; const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] }; const mock = new MockAdapter(axios); + const commit = jasmine.createSpy('commit'); mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); - testAction( - loadCollapsedDiff, - file, - {}, - [ - { - type: types.ADD_COLLAPSED_DIFFS, - payload: { file, data }, - }, - ], - [], - () => { + loadCollapsedDiff({ commit, getters: { commitId: null } }, file) + .then(() => { + expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data }); + mock.restore(); done(); - }, - ); + }) + .catch(done.fail); + }); + + it('should fetch data without commit ID', () => { + const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; + const getters = { + commitId: null, + }; + + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); + + loadCollapsedDiff({ commit() {}, getters }, file); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: { commit_id: null }, + }); + }); + + it('should fetch data with commit ID', () => { + const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' }; + const getters = { + commitId: '123', + }; + + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); + + loadCollapsedDiff({ commit() {}, getters }, file); + + expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, { + params: { commit_id: '123' }, + }); }); }); @@ -469,7 +504,7 @@ describe('DiffsStoreActions', () => { describe('scrollToLineIfNeededInline', () => { const lineMock = { - lineCode: 'ABC_123', + line_code: 'ABC_123', }; it('should not call handleLocationHash when there is not hash', () => { @@ -520,7 +555,7 @@ describe('DiffsStoreActions', () => { const lineMock = { left: null, right: { - lineCode: 'ABC_123', + line_code: 'ABC_123', }, }; @@ -575,11 +610,18 @@ describe('DiffsStoreActions', () => { }); describe('saveDiffDiscussion', () => { - beforeEach(() => { - spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData'); - }); - it('dispatches actions', done => { + const commitId = 'something'; + const formData = { + diffFile: { ...mockDiffFile }, + noteableData: {}, + }; + const note = {}; + const state = { + commit: { + id: commitId, + }, + }; const dispatch = jasmine.createSpy('dispatch').and.callFake(name => { switch (name) { case 'saveNote': @@ -593,11 +635,19 @@ describe('DiffsStoreActions', () => { } }); - saveDiffDiscussion({ dispatch }, { note: {}, formData: {} }) + saveDiffDiscussion({ state, dispatch }, { note, formData }) .then(() => { - expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]); - expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); - expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]); + const { calls } = dispatch; + + expect(calls.count()).toBe(5); + expect(calls.argsFor(0)).toEqual(['saveNote', jasmine.any(Object), { root: true }]); + + const postData = calls.argsFor(0)[1]; + + expect(postData.data.note.commit_id).toBe(commitId); + + expect(calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); + expect(calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]); }) .then(done) .catch(done.fail); @@ -687,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/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index eef95c823fb..582535e0a53 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -186,77 +186,6 @@ describe('Diffs Module Getters', () => { }); }); - describe('shouldRenderParallelCommentRow', () => { - let line; - - beforeEach(() => { - line = {}; - - discussionMock.expanded = true; - - line.left = { - line_code: 'ABC', - discussions: [discussionMock], - }; - - line.right = { - line_code: 'DEF', - discussions: [discussionMock1], - }; - }); - - it('returns true when discussion is expanded', () => { - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true); - }); - - it('returns false when no discussion was found', () => { - line.left.discussions = []; - line.right.discussions = []; - - localState.diffLineCommentForms.ABC = false; - localState.diffLineCommentForms.DEF = false; - - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(false); - }); - - it('returns true when discussionForm was found', () => { - localState.diffLineCommentForms.ABC = {}; - - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true); - }); - }); - - describe('shouldRenderInlineCommentRow', () => { - let line; - - beforeEach(() => { - discussionMock.expanded = true; - - line = { - lineCode: 'ABC', - discussions: [discussionMock], - }; - }); - - it('returns true when diffLineCommentForms has form', () => { - localState.diffLineCommentForms.ABC = {}; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true); - }); - - it('returns false when no line discussions were found', () => { - line.discussions = []; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(false); - }); - - it('returns true if all found discussions are expanded', () => { - discussionMock.expanded = true; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true); - }); - }); - describe('getDiffFileDiscussions', () => { it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => { discussionMock.diff_file.file_hash = diffFileMock.file_hash; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 598d723c940..d8733941181 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -55,32 +55,6 @@ describe('DiffsStoreMutations', () => { }); }); - describe('ADD_COMMENT_FORM_LINE', () => { - it('should set a truthy reference for the given line code in diffLineCommentForms', () => { - const state = { diffLineCommentForms: {} }; - const lineCode = 'FDE'; - - mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); - }); - }); - - describe('REMOVE_COMMENT_FORM_LINE', () => { - it('should remove given reference from diffLineCommentForms', () => { - const state = { diffLineCommentForms: {} }; - const lineCode = 'FDE'; - - mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); - - mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeUndefined(); - }); - }); - describe('EXPAND_ALL_FILES', () => { it('should change the collapsed prop from diffFiles', () => { const diffFile = { @@ -98,7 +72,9 @@ describe('DiffsStoreMutations', () => { it('should call utils.addContextLines with proper params', () => { const options = { lineNumbers: { oldLineNumber: 1, newLineNumber: 2 }, - contextLines: [{ old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [] }], + contextLines: [ + { old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [], hasForm: false }, + ], fileHash: 'ff9200', params: { bottom: true, @@ -223,6 +199,230 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); }); + it('should not duplicate discussions on line', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + }); + + it('updates existing discussion', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallel_diff_lines[0].right.discussions).toEqual([]); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion: { + ...discussion, + resolved: true, + notes: ['test'], + }, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].notes.length).toBe(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].notes.length).toBe(1); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].resolved).toBe(true); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].resolved).toBe(true); + }); + + it('should not duplicate inline diff discussions', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [ + { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: 'ABC', + }, + }, + ], + }, + { + line_code: 'ABC_2', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 2, + line_code: 'ABC_2', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_2: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toBe(1); + }); + it('should add legacy discussions to the given line', () => { const diffPosition = { base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', @@ -302,10 +502,12 @@ describe('DiffsStoreMutations', () => { { id: 1, line_code: 'ABC_1', + notes: [], }, { id: 2, line_code: 'ABC_1', + notes: [], }, ], }, @@ -322,10 +524,12 @@ describe('DiffsStoreMutations', () => { { id: 1, line_code: 'ABC_1', + notes: [], }, { id: 2, line_code: 'ABC_1', + notes: [], }, ], }, @@ -383,4 +587,45 @@ describe('DiffsStoreMutations', () => { expect(state.currentDiffFileId).toBe('somefileid'); }); }); + + describe('Set highlighted row', () => { + it('sets highlighted row', () => { + const state = createState(); + + mutations[types.SET_HIGHLIGHTED_ROW](state, 'ABC_123'); + + expect(state.highlightedRow).toBe('ABC_123'); + }); + }); + + describe('TOGGLE_LINE_HAS_FORM', () => { + it('sets hasForm on lines', () => { + const file = { + file_hash: 'hash', + parallel_diff_lines: [ + { left: { line_code: '123', hasForm: false }, right: {} }, + { left: {}, right: { line_code: '124', hasForm: false } }, + ], + highlighted_diff_lines: [ + { line_code: '123', hasForm: false }, + { line_code: '124', hasForm: false }, + ], + }; + const state = { + diffFiles: [file], + }; + + mutations[types.TOGGLE_LINE_HAS_FORM](state, { + lineCode: '123', + hasForm: true, + fileHash: 'hash', + }); + + expect(file.highlighted_diff_lines[0].hasForm).toBe(true); + expect(file.highlighted_diff_lines[1].hasForm).toBe(false); + + expect(file.parallel_diff_lines[0].left.hasForm).toBe(true); + expect(file.parallel_diff_lines[1].right.hasForm).toBe(false); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index d4ef17c5ef8..4268634d302 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -150,7 +150,7 @@ describe('DiffsStoreUtils', () => { note: { noteable_type: options.noteableType, noteable_id: options.noteableData.id, - commit_id: '', + commit_id: undefined, type: DIFF_NOTE_TYPE, line_code: options.noteTargetLine.line_code, note: options.note, @@ -209,7 +209,7 @@ describe('DiffsStoreUtils', () => { note: { noteable_type: options.noteableType, noteable_id: options.noteableData.id, - commit_id: '', + commit_id: undefined, type: LEGACY_DIFF_NOTE_TYPE, line_code: options.noteTargetLine.line_code, note: options.note, @@ -294,10 +294,14 @@ describe('DiffsStoreUtils', () => { }); describe('prepareDiffData', () => { - it('sets the renderIt and collapsed attribute on files', () => { - const preparedDiff = { diff_files: [getDiffFileMock()] }; + let preparedDiff; + + beforeEach(() => { + preparedDiff = { diff_files: [getDiffFileMock()] }; utils.prepareDiffData(preparedDiff); + }); + it('sets the renderIt and collapsed attribute on files', () => { const firstParallelDiffLine = preparedDiff.diff_files[0].parallel_diff_lines[2]; expect(firstParallelDiffLine.left.discussions.length).toBe(0); @@ -323,6 +327,18 @@ describe('DiffsStoreUtils', () => { expect(preparedDiff.diff_files[0].renderIt).toBeTruthy(); expect(preparedDiff.diff_files[0].collapsed).toBeFalsy(); }); + + it('adds line_code to all lines', () => { + expect( + preparedDiff.diff_files[0].parallel_diff_lines.filter(line => !line.line_code), + ).toHaveLength(0); + }); + + it('uses right line code if left has none', () => { + const firstLine = preparedDiff.diff_files[0].parallel_diff_lines[0]; + + expect(firstLine.line_code).toEqual(firstLine.right.line_code); + }); }); describe('isDiscussionApplicableToLine', () => { @@ -559,4 +575,26 @@ describe('DiffsStoreUtils', () => { ]); }); }); + + describe('getDiffMode', () => { + it('returns mode when matched in file', () => { + expect( + utils.getDiffMode({ + renamed_file: true, + }), + ).toBe('renamed'); + }); + + it('returns mode_changed if key has no match', () => { + expect( + utils.getDiffMode({ + mode_changed: true, + }), + ).toBe('mode_changed'); + }); + + it('defaults to replaced', () => { + expect(utils.getDiffMode({})).toBe('replaced'); + }); + }); }); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js index f1576b19d1b..56e18db59c5 100644 --- a/spec/javascripts/environments/environment_terminal_button_spec.js +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -2,30 +2,46 @@ import Vue from 'vue'; import terminalComp from '~/environments/components/environment_terminal_button.vue'; describe('Stop Component', () => { - let TerminalComponent; let component; const terminalPath = '/path'; - beforeEach(() => { - TerminalComponent = Vue.extend(terminalComp); - + const mountWithProps = props => { + const TerminalComponent = Vue.extend(terminalComp); component = new TerminalComponent({ - propsData: { - terminalPath, - }, + propsData: props, }).$mount(); - }); + }; + + describe('enabled', () => { + beforeEach(() => { + mountWithProps({ terminalPath }); + }); + + describe('computed', () => { + it('title', () => { + expect(component.title).toEqual('Terminal'); + }); + }); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Terminal'); + it('should render a link to open a web terminal with the provided path', () => { + expect(component.$el.tagName).toEqual('A'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); + expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); + expect(component.$el.getAttribute('href')).toEqual(terminalPath); + }); + + it('should render a non-disabled button', () => { + expect(component.$el.classList).not.toContain('disabled'); }); }); - it('should render a link to open a web terminal with the provided path', () => { - expect(component.$el.tagName).toEqual('A'); - expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal'); - expect(component.$el.getAttribute('aria-label')).toEqual('Terminal'); - expect(component.$el.getAttribute('href')).toEqual(terminalPath); + describe('disabled', () => { + beforeEach(() => { + mountWithProps({ terminalPath, disabled: true }); + }); + + it('should render a disabled button', () => { + expect(component.$el.classList).toContain('disabled'); + }); }); }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 6605b0a30d7..cfd0b96ec43 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -211,132 +211,6 @@ describe('Dropdown Utils', () => { }); }); - describe('mergeDuplicateLabels', () => { - const dataMap = { - label: { - title: 'label', - color: '#FFFFFF', - }, - }; - - it('should add label to dataMap if it is not a duplicate', () => { - const newLabel = { - title: 'new-label', - color: '#000000', - }; - - const updated = DropdownUtils.mergeDuplicateLabels(dataMap, newLabel); - - expect(updated[newLabel.title]).toEqual(newLabel); - }); - - it('should merge colors if label is a duplicate', () => { - const duplicate = { - title: 'label', - color: '#000000', - }; - - const updated = DropdownUtils.mergeDuplicateLabels(dataMap, duplicate); - - expect(updated.label.multipleColors).toEqual([dataMap.label.color, duplicate.color]); - }); - }); - - describe('duplicateLabelColor', () => { - it('should linear-gradient 2 colors', () => { - const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']); - - expect(gradient).toEqual( - 'linear-gradient(#FFFFFF 0%, #FFFFFF 50%, #000000 50%, #000000 100%)', - ); - }); - - it('should linear-gradient 3 colors', () => { - const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']); - - expect(gradient).toEqual( - 'linear-gradient(#FFFFFF 0%, #FFFFFF 33%, #000000 33%, #000000 66%, #333333 66%, #333333 100%)', - ); - }); - - it('should linear-gradient 4 colors', () => { - const gradient = DropdownUtils.duplicateLabelColor([ - '#FFFFFF', - '#000000', - '#333333', - '#DDDDDD', - ]); - - expect(gradient).toEqual( - 'linear-gradient(#FFFFFF 0%, #FFFFFF 25%, #000000 25%, #000000 50%, #333333 50%, #333333 75%, #DDDDDD 75%, #DDDDDD 100%)', - ); - }); - - it('should not linear-gradient more than 4 colors', () => { - const gradient = DropdownUtils.duplicateLabelColor([ - '#FFFFFF', - '#000000', - '#333333', - '#DDDDDD', - '#EEEEEE', - ]); - - expect(gradient.indexOf('#EEEEEE')).toBe(-1); - }); - }); - - describe('duplicateLabelPreprocessing', () => { - it('should set preprocessed to true', () => { - const results = DropdownUtils.duplicateLabelPreprocessing([]); - - expect(results.preprocessed).toEqual(true); - }); - - it('should not mutate existing data if there are no duplicates', () => { - const data = [ - { - title: 'label1', - color: '#FFFFFF', - }, - { - title: 'label2', - color: '#000000', - }, - ]; - const results = DropdownUtils.duplicateLabelPreprocessing(data); - - expect(results.length).toEqual(2); - expect(results[0]).toEqual(data[0]); - expect(results[1]).toEqual(data[1]); - }); - - describe('duplicate labels', () => { - const data = [ - { - title: 'label', - color: '#FFFFFF', - }, - { - title: 'label', - color: '#000000', - }, - ]; - const results = DropdownUtils.duplicateLabelPreprocessing(data); - - it('should merge duplicate labels', () => { - expect(results.length).toEqual(1); - }); - - it('should convert multiple colored labels into linear-gradient', () => { - expect(results[0].color).toEqual(DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000'])); - }); - - it('should set multiple colored label text color to black', () => { - expect(results[0].text_color).toEqual('#000000'); - }); - }); - }); - describe('setDataValueIfSelected', () => { beforeEach(() => { spyOn(FilteredSearchDropdownManager, 'addWordToInput').and.callFake(() => {}); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 4f561df7943..9aa3cbaa231 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -909,16 +909,6 @@ describe('Filtered Search Visual Tokens', () => { expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor); }); - it('should not set backgroundColor when it is a linear-gradient', () => { - const token = subject.setTokenStyle( - bugLabelToken, - 'linear-gradient(135deg, red, blue)', - 'white', - ); - - expect(token.style.backgroundColor).toEqual(bugLabelToken.style.backgroundColor); - }); - it('should set textColor', () => { const token = subject.setTokenStyle(bugLabelToken, 'white', 'black'); @@ -935,39 +925,6 @@ describe('Filtered Search Visual Tokens', () => { }); }); - describe('preprocessLabel', () => { - const endpoint = 'endpoint'; - - it('does not preprocess more than once', () => { - let labels = []; - - spyOn(DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []); - - labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - - expect(DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1); - }); - - describe('not preprocessed before', () => { - it('returns preprocessed labels', () => { - let labels = []; - - expect(labels.preprocessed).not.toEqual(true); - labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - - expect(labels.preprocessed).toEqual(true); - }); - - it('overrides AjaxCache with preprocessed results', () => { - spyOn(AjaxCache, 'override').and.callFake(() => {}); - FilteredSearchVisualTokens.preprocessLabel(endpoint, []); - - expect(AjaxCache.override.calls.count()).toEqual(1); - }); - }); - }); - describe('updateLabelTokenColor', () => { const jsonFixtureName = 'labels/project_labels.json'; const dummyEndpoint = '/dummy/endpoint'; diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js index 04925d37ac4..9e2ba1f5ce9 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -14,10 +14,14 @@ import testAction from '../../../../helpers/vuex_action_helper'; describe('IDE merge requests actions', () => { let mockedState; + let mockedRootState; let mock; beforeEach(() => { mockedState = state(); + mockedRootState = { + currentProjectId: 7, + }; mock = new MockAdapter(axios); }); @@ -86,13 +90,16 @@ describe('IDE merge requests actions', () => { describe('success', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); + mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests); }); it('calls API with params', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' }); + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: 'created' }, + ); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { @@ -107,7 +114,7 @@ describe('IDE merge requests actions', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); fetchMergeRequests( - { dispatch() {}, state: mockedState }, + { dispatch() {}, state: mockedState, rootState: mockedRootState }, { type: 'created', search: 'testing search' }, ); @@ -139,6 +146,49 @@ describe('IDE merge requests actions', () => { }); }); + describe('success without type', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests); + }); + + it('calls API with project', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: null, search: 'testing search' }, + ); + + expect(apiSpy).toHaveBeenCalledWith( + jasmine.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`), + { + params: { + state: 'opened', + search: 'testing search', + }, + }, + ); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + { type: null }, + { ...mockedState, ...mockedRootState }, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { + type: 'receiveMergeRequestsSuccess', + payload: mergeRequests, + }, + ], + done, + ); + }); + }); + describe('error', () => { beforeEach(() => { mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500); diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js index 8ea05203d00..b3001d45e3c 100644 --- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js +++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js @@ -61,6 +61,10 @@ describe('badge helper', () => { expect(buttonEl).toBeDefined(); }); + it('should add badge classes', () => { + expect(buttonEl.className).toContain('badge badge-pill'); + }); + it('should set the badge text', () => { expect(buttonEl.innerText).toEqual(badgeText); }); diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/javascripts/jobs/components/artifacts_block_spec.js index 2fa7ff653fe..27d480ef2ea 100644 --- a/spec/javascripts/jobs/components/artifacts_block_spec.js +++ b/spec/javascripts/jobs/components/artifacts_block_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import { getTimeago } from '~/lib/utils/datetime_utility'; import component from '~/jobs/components/artifacts_block.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; +import { trimText } from '../../helpers/vue_component_helper'; describe('Artifacts block', () => { const Component = Vue.extend(component); @@ -9,7 +10,7 @@ describe('Artifacts block', () => { const expireAt = '2018-08-14T09:38:49.157Z'; const timeago = getTimeago(); - const formatedDate = timeago.format(expireAt); + const formattedDate = timeago.format(expireAt); const expiredArtifact = { expire_at: expireAt, @@ -36,9 +37,8 @@ describe('Artifacts block', () => { expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull(); expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull(); - expect(vm.$el.textContent).toContain(formatedDate); - expect(vm.$el.querySelector('.js-artifacts-removed').textContent.trim()).toEqual( - 'The artifacts were removed', + expect(trimText(vm.$el.querySelector('.js-artifacts-removed').textContent)).toEqual( + `The artifacts were removed ${formattedDate}`, ); }); }); @@ -51,9 +51,8 @@ describe('Artifacts block', () => { expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull(); expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull(); - expect(vm.$el.textContent).toContain(formatedDate); - expect(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent.trim()).toEqual( - 'The artifacts will be removed in', + expect(trimText(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent)).toEqual( + `The artifacts will be removed ${formattedDate}`, ); }); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index fcf3780f0ea..ba5d672f189 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -160,9 +160,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners that can run this job.", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner')).not.toBeNull(); done(); }, 0); }); @@ -195,9 +193,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); @@ -230,9 +226,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 424092d2d88..b0bc16d7c64 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -79,14 +79,6 @@ describe('Sidebar details block', () => { }); describe('information', () => { - it('should render merge request link', () => { - expect(trimText(vm.$el.querySelector('.js-job-mr').textContent)).toEqual('Merge Request: !2'); - - expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual( - job.merge_request.path, - ); - }); - it('should render job duration', () => { expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual( 'Duration: 6 seconds', 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/file_upload_spec.js b/spec/javascripts/lib/utils/file_upload_spec.js new file mode 100644 index 00000000000..92c9cc70aaf --- /dev/null +++ b/spec/javascripts/lib/utils/file_upload_spec.js @@ -0,0 +1,36 @@ +import fileUpload from '~/lib/utils/file_upload'; + +describe('File upload', () => { + beforeEach(() => { + setFixtures(` + <form> + <button class="js-button" type="button">Click me!</button> + <input type="text" class="js-input" /> + <span class="js-filename"></span> + </form> + `); + + fileUpload('.js-button', '.js-input'); + }); + + it('clicks file input after clicking button', () => { + const btn = document.querySelector('.js-button'); + const input = document.querySelector('.js-input'); + + spyOn(input, 'click'); + + btn.click(); + + expect(input.click).toHaveBeenCalled(); + }); + + it('updates file name text', () => { + const input = document.querySelector('.js-input'); + + input.value = 'path/to/file/index.js'; + + input.dispatchEvent(new CustomEvent('change')); + + expect(document.querySelector('.js-filename').textContent).toEqual('index.js'); + }); +}); diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js index c7f4092911c..e4df8441793 100644 --- a/spec/javascripts/lib/utils/url_utility_spec.js +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -1,4 +1,4 @@ -import { webIDEUrl } from '~/lib/utils/url_utility'; +import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility'; describe('URL utility', () => { describe('webIDEUrl', () => { @@ -26,4 +26,26 @@ describe('URL utility', () => { }); }); }); + + describe('mergeUrlParams', () => { + it('adds w', () => { + expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); + expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); + expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); + }); + + it('updates w', () => { + expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); + }); + + it('adds multiple params', () => { + expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); + }); + + it('adds and updates encoded params', () => { + expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + }); + }); }); 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/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 4cc18afdf24..59d6d4f3a7f 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -5,6 +5,7 @@ import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries, + queryWithoutData, } from './mock_data'; const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; @@ -104,4 +105,23 @@ describe('Graph', () => { expect(component.currentData).toBe(component.timeSeries[0].values[10]); }); + + describe('Without data to display', () => { + it('shows a "no data to display" empty state on a graph', done => { + const component = createComponent({ + graphData: queryWithoutData, + deploymentData, + tagsPath, + projectPath, + }); + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.js-no-data-to-display text').textContent.trim(), + ).toEqual('No data to display'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 6c833b17f98..18ad9843d22 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = { queries: [ { query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', - y_label: 'Memory', + label: 'Memory', unit: 'MiB', result: [ { @@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = { ], }, { + id: 6, title: 'CPU usage', weight: 1, queries: [ { query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', + label: 'Core Usage', + unit: 'Cores', result: [ { metric: {}, @@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = { }, ], }, + { + group: 'NGINX', + priority: 2, + metrics: [ + { + id: 100, + title: 'Http Error Rate', + weight: 100, + queries: [ + { + query_range: + 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100', + label: '5xx errors', + unit: '%', + result: [ + { + metric: {}, + values: [ + [1495700554.925, NaN], + [1495700614.925, NaN], + [1495700674.925, NaN], + [1495700734.925, NaN], + [1495700794.925, NaN], + [1495700854.925, NaN], + [1495700914.925, NaN], + ], + }, + ], + }, + ], + }, + ], + }, ], last_update: '2017-05-25T13:18:34.949Z', }; @@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [ }, ]; +export const queryWithoutData = { + title: 'HTTP Error rate', + weight: 10, + y_label: 'Http Error Rate', + queries: [ + { + query_range: + 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100', + label: '5xx errors', + unit: '%', + result: [], + }, + ], +}; + export function convertDatesMultipleSeries(multipleSeries) { const convertedMultiple = multipleSeries; multipleSeries.forEach((column, index) => { diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js index bf68c911549..d8a980c874d 100644 --- a/spec/javascripts/monitoring/monitoring_store_spec.js +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -1,31 +1,35 @@ import MonitoringStore from '~/monitoring/stores/monitoring_store'; import MonitoringMock, { deploymentData, environmentData } from './mock_data'; -describe('MonitoringStore', function() { - this.store = new MonitoringStore(); - this.store.storeMetrics(MonitoringMock.data); - - it('contains one group that contains two queries sorted by priority', () => { - expect(this.store.groups).toBeDefined(); - expect(this.store.groups.length).toEqual(1); - expect(this.store.groups[0].metrics.length).toEqual(2); +describe('MonitoringStore', () => { + const store = new MonitoringStore(); + store.storeMetrics(MonitoringMock.data); + + it('contains two groups that contains, one of which has two queries sorted by priority', () => { + expect(store.groups).toBeDefined(); + expect(store.groups.length).toEqual(2); + expect(store.groups[0].metrics.length).toEqual(2); }); it('gets the metrics count for every group', () => { - expect(this.store.getMetricsCount()).toEqual(2); + expect(store.getMetricsCount()).toEqual(3); }); it('contains deployment data', () => { - this.store.storeDeploymentData(deploymentData); + store.storeDeploymentData(deploymentData); - expect(this.store.deploymentData).toBeDefined(); - expect(this.store.deploymentData.length).toEqual(3); - expect(typeof this.store.deploymentData[0]).toEqual('object'); + expect(store.deploymentData).toBeDefined(); + expect(store.deploymentData.length).toEqual(3); + expect(typeof store.deploymentData[0]).toEqual('object'); }); it('only stores environment data that contains deployments', () => { - this.store.storeEnvironmentsData(environmentData); + store.storeEnvironmentsData(environmentData); + + expect(store.environmentsData.length).toEqual(2); + }); - expect(this.store.environmentsData.length).toEqual(2); + it('removes the data if all the values from a query are not defined', () => { + expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0); }); }); diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index 95461396f10..0752bd05904 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -17,7 +17,7 @@ describe('diff_with_note', () => { }; const selectors = { get container() { - return vm.$refs.fileHolder; + return vm.$el; }, get diffTable() { return this.container.querySelector('.diff-content table'); @@ -70,7 +70,6 @@ describe('diff_with_note', () => { it('shows image diff', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(selectors.container).toHaveClass('js-image-file'); expect(selectors.diffTable).not.toExist(); }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 0081f42c330..22bee049f9c 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -30,6 +30,8 @@ describe('note_app', () => { jasmine.addMatchers(vueMatchers); $('body').attr('data-page', 'projects:merge_requests:show'); + setFixtures('<div class="js-vue-notes-event"><div id="app"></div></div>'); + const IssueNotesApp = Vue.extend(notesApp); store = createStore(); @@ -43,6 +45,7 @@ describe('note_app', () => { return mountComponentWithStore(IssueNotesApp, { props, store, + el: document.getElementById('app'), }); }; }); @@ -283,4 +286,24 @@ describe('note_app', () => { }, 0); }); }); + + describe('emoji awards', () => { + it('dispatches toggleAward after toggleAward event', () => { + const toggleAwardEvent = new CustomEvent('toggleAward', { + detail: { + awardName: 'test', + noteId: 1, + }, + }); + + spyOn(vm.$store, 'dispatch'); + + vm.$el.parentElement.dispatchEvent(toggleAwardEvent); + + expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleAward', { + awardName: 'test', + noteId: 1, + }); + }); + }); }); 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 4b4403689d9..106a4ac2546 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -6,7 +6,6 @@ import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_file'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; -const diffDiscussionFixture = 'merge_requests/diff_discussion.json'; describe('noteable_discussion component', () => { const Component = Vue.extend(noteableDiscussion); @@ -43,12 +42,14 @@ describe('noteable_discussion component', () => { const discussion = { ...discussionMock }; discussion.diff_file = mockDiffFile; discussion.diff_discussion = true; - const diffDiscussionVm = new Component({ + + vm.$destroy(); + vm = new Component({ store, propsData: { discussion }, }).$mount(); - expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull(); + expect(vm.$el.querySelector('.discussion-header')).not.toBeNull(); }); describe('actions', () => { @@ -79,93 +80,12 @@ describe('noteable_discussion component', () => { }); }); - describe('computed', () => { - describe('hasMultipleUnresolvedDiscussions', () => { - it('is false if there are no unresolved discussions', done => { - spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]); - - Vue.nextTick() - .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); - }) - .then(done) - .catch(done.fail); - }); - - it('is false if there is one unresolved discussion', done => { - spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]); - - Vue.nextTick() - .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); - }) - .then(done) - .catch(done.fail); - }); - - it('is true if there are two unresolved discussions', done => { - const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; - discussion.notes[0].resolved = false; - vm.$store.dispatch('setInitialNotes', [discussion, discussion]); - - Vue.nextTick() - .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(true); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('isRepliesCollapsed', () => { - it('should return false for diff discussions', done => { - const diffDiscussion = getJSONFixture(diffDiscussionFixture)[0]; - vm.$store.dispatch('setInitialNotes', [diffDiscussion]); - - Vue.nextTick() - .then(() => { - expect(vm.isRepliesCollapsed).toEqual(false); - expect(vm.$el.querySelector('.js-toggle-replies')).not.toBeNull(); - expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - - it('should return false if discussion does not have a reply', () => { - const discussion = { ...discussionMock, resolved: true }; - discussion.notes = discussion.notes.slice(0, 1); - const noRepliesVm = new Component({ - store, - propsData: { discussion }, - }).$mount(); - - expect(noRepliesVm.isRepliesCollapsed).toEqual(false); - expect(noRepliesVm.$el.querySelector('.js-toggle-replies')).toBeNull(); - expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); - noRepliesVm.$destroy(); - }); - - it('should return true for resolved non-diff discussion which has replies', () => { - const discussion = { ...discussionMock, resolved: true }; - const resolvedDiscussionVm = new Component({ - store, - propsData: { discussion }, - }).$mount(); - - expect(resolvedDiscussionVm.isRepliesCollapsed).toEqual(true); - expect(resolvedDiscussionVm.$el.querySelector('.js-toggle-replies')).not.toBeNull(); - expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); - resolvedDiscussionVm.$destroy(); - }); - }); - }); - describe('methods', () => { describe('jumpToNextDiscussion', () => { 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'; @@ -212,4 +132,44 @@ describe('noteable_discussion component', () => { expect(note).toEqual(data); }); }); + + describe('commit discussion', () => { + const commitId = 'razupaltuff'; + + beforeEach(() => { + vm.$destroy(); + + store.state.diffs = { + projectPath: 'something', + }; + + vm.$destroy(); + vm = new Component({ + propsData: { + discussion: { + ...discussionMock, + for_commit: true, + commit_id: commitId, + diff_discussion: true, + diff_file: { + ...mockDiffFile, + }, + }, + renderDiffFile: true, + }, + store, + }).$mount(); + }); + + it('displays a monospace started a discussion on commit', () => { + const truncatedCommitId = commitId.substr(0, 8); + + expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`); + + const commitElement = vm.$el.querySelector('.commit-sha'); + + expect(commitElement).not.toBe(null); + expect(commitElement).toHaveText(truncatedCommitId); + }); + }); }); 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 fcdd834e4a0..2e3cd5e8f36 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import $ from 'jquery'; import _ from 'underscore'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; @@ -123,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, ); }); @@ -330,10 +331,14 @@ describe('Actions Notes Store', () => { beforeEach(() => { Vue.http.interceptors.push(interceptor); + + $('body').attr('data-page', ''); }); afterEach(() => { Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + + $('body').attr('data-page', ''); }); it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => { @@ -353,6 +358,39 @@ describe('Actions Notes Store', () => { { type: 'updateMergeRequestWidget', }, + { + type: 'updateResolvableDiscussonsCounts', + }, + ], + done, + ); + }); + + it('dispatches removeDiscussionsFromDiff on merge request page', done => { + const note = { path: `${gl.TEST_HOST}`, id: 1 }; + + $('body').attr('data-page', 'projects:merge_requests:show'); + + testAction( + actions.deleteNote, + note, + store.state, + [ + { + type: 'DELETE_NOTE', + payload: note, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + { + type: 'updateResolvableDiscussonsCounts', + }, + { + type: 'diffs/removeDiscussionsFromDiff', + }, ], done, ); @@ -399,6 +437,9 @@ describe('Actions Notes Store', () => { { type: 'startTaskList', }, + { + type: 'updateResolvableDiscussonsCounts', + }, ], done, ); @@ -472,6 +513,9 @@ describe('Actions Notes Store', () => { ], [ { + type: 'updateResolvableDiscussonsCounts', + }, + { type: 'updateMergeRequestWidget', }, ], @@ -494,6 +538,9 @@ describe('Actions Notes Store', () => { ], [ { + type: 'updateResolvableDiscussonsCounts', + }, + { type: 'updateMergeRequestWidget', }, ], @@ -525,4 +572,17 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('updateResolvableDiscussonsCounts', () => { + it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', done => { + testAction( + actions.updateResolvableDiscussonsCounts, + null, + {}, + [{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index f853f9ff088..c066975a43b 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -117,17 +117,15 @@ describe('Getters Notes Store', () => { describe('allResolvableDiscussions', () => { it('should return only resolvable discussions in same order', () => { - const localGetters = { - allDiscussions: [ - discussion3, - unresolvableDiscussion, - discussion1, - unresolvableDiscussion, - discussion2, - ], - }; + state.discussions = [ + discussion3, + unresolvableDiscussion, + discussion1, + unresolvableDiscussion, + discussion2, + ]; - expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([ + expect(getters.allResolvableDiscussions(state)).toEqual([ discussion3, discussion1, discussion2, @@ -135,11 +133,9 @@ describe('Getters Notes Store', () => { }); it('should return empty array if there are no resolvable discussions', () => { - const localGetters = { - allDiscussions: [unresolvableDiscussion, unresolvableDiscussion], - }; + state.discussions = [unresolvableDiscussion, unresolvableDiscussion]; - expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([]); + expect(getters.allResolvableDiscussions(state)).toEqual([]); }); }); @@ -236,7 +232,7 @@ describe('Getters Notes Store', () => { it('should return the ID of the discussion after the ID provided', () => { expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456'); expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789'); - expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe(undefined); + expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123'); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 461de5a3106..3fbae82f16c 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -9,6 +9,11 @@ import { individualNote, } from '../mock_data'; +const RESOLVED_NOTE = { resolvable: true, resolved: true }; +const UNRESOLVED_NOTE = { resolvable: true, resolved: false }; +const SYSTEM_NOTE = { resolvable: false, resolved: false }; +const WEIRD_NOTE = { resolvable: false, resolved: true }; + describe('Notes Store mutations', () => { describe('ADD_NEW_NOTE', () => { let state; @@ -297,6 +302,16 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].expanded).toEqual(false); }); + + it('forces a discussions expanded state', () => { + const state = { + discussions: [{ ...discussionMock, expanded: false }], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id, forceExpanded: true }); + + expect(state.discussions[0].expanded).toEqual(true); + }); }); describe('UPDATE_NOTE', () => { @@ -437,4 +452,63 @@ describe('Notes Store mutations', () => { expect(state.commentsDisabled).toEqual(true); }); }); + + describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => { + it('with unresolvable discussions, updates state', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: true, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: false, resolvable: false, notes: [UNRESOLVED_NOTE] }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + jasmine.objectContaining({ + resolvableDiscussionsCount: 1, + unresolvedDiscussionsCount: 1, + hasUnresolvedDiscussions: false, + }), + ); + }); + + it('with resolvable discussions, updates state', () => { + const state = { + discussions: [ + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, RESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, WEIRD_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [SYSTEM_NOTE, RESOLVED_NOTE, WEIRD_NOTE, UNRESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [UNRESOLVED_NOTE], + }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + jasmine.objectContaining({ + resolvableDiscussionsCount: 4, + unresolvedDiscussionsCount: 2, + hasUnresolvedDiscussions: true, + }), + ); + }); + }); }); diff --git a/spec/javascripts/pages/profiles/show/emoji_menu_spec.js b/spec/javascripts/pages/profiles/show/emoji_menu_spec.js deleted file mode 100644 index 864bda65736..00000000000 --- a/spec/javascripts/pages/profiles/show/emoji_menu_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; -import EmojiMenu from '~/pages/profiles/show/emoji_menu'; -import { TEST_HOST } from 'spec/test_constants'; - -describe('EmojiMenu', () => { - const dummyEmojiTag = '<dummy></tag>'; - const dummyToggleButtonSelector = '.toggle-button-selector'; - const dummyMenuClass = 'dummy-menu-class'; - - let emojiMenu; - let dummySelectEmojiCallback; - let dummyEmojiList; - - beforeEach(() => { - dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback'); - dummyEmojiList = { - glEmojiTag() { - return dummyEmojiTag; - }, - normalizeEmojiName(emoji) { - return emoji; - }, - isEmojiNameValid() { - return true; - }, - getEmojiCategoryMap() { - return { dummyCategory: [] }; - }, - }; - - emojiMenu = new EmojiMenu( - dummyEmojiList, - dummyToggleButtonSelector, - dummyMenuClass, - dummySelectEmojiCallback, - ); - }); - - afterEach(() => { - emojiMenu.destroy(); - }); - - describe('addAward', () => { - const dummyAwardUrl = `${TEST_HOST}/award/url`; - const dummyEmoji = 'tropical_fish'; - const dummyVotesBlock = () => $('<div />'); - - it('calls selectEmojiCallback', done => { - expect(dummySelectEmojiCallback).not.toHaveBeenCalled(); - - emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => { - expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag); - done(); - }); - }); - - it('does not make an axios requst', done => { - spyOn(axios, 'request').and.stub(); - - emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => { - expect(axios.request).not.toHaveBeenCalled(); - done(); - }); - }); - }); - - describe('bindEvents', () => { - beforeEach(() => { - spyOn(emojiMenu, 'registerEventListener').and.stub(); - }); - - it('binds event listeners to custom toggle button', () => { - emojiMenu.bindEvents(); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'one', - jasmine.anything(), - 'mouseenter focus', - dummyToggleButtonSelector, - 'mouseenter focus', - jasmine.anything(), - ); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'on', - jasmine.anything(), - 'click', - dummyToggleButtonSelector, - jasmine.anything(), - ); - }); - - it('binds event listeners to custom menu class', () => { - emojiMenu.bindEvents(); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'on', - jasmine.anything(), - 'click', - `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`, - jasmine.anything(), - ); - }); - }); - - describe('createEmojiMenu', () => { - it('renders the menu with custom menu class', () => { - const menuElement = () => - document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`); - - expect(menuElement()).toBe(null); - - emojiMenu.createEmojiMenu(); - - expect(menuElement()).not.toBe(null); - }); - }); -}); diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js index 88e1789184d..1cdb0aff524 100644 --- a/spec/javascripts/pipelines/graph/job_item_spec.js +++ b/spec/javascripts/pipelines/graph/job_item_spec.js @@ -139,57 +139,17 @@ describe('pipeline graph job item', () => { }); }); - describe('tooltip placement', () => { - it('does not set tooltip boundary by default', () => { - component = mountComponent(JobComponent, { - job: mockJob, - }); - - expect(component.tooltipBoundary).toBeNull(); - }); - - it('sets tooltip boundary to viewport for small dropdowns', () => { - component = mountComponent(JobComponent, { - job: mockJob, - dropdownLength: 1, - }); - - expect(component.tooltipBoundary).toEqual('viewport'); - }); - - it('does not set tooltip boundary for large lists', () => { - component = mountComponent(JobComponent, { - job: mockJob, - dropdownLength: 7, - }); - - expect(component.tooltipBoundary).toBeNull(); - }); - }); - describe('for delayed job', () => { - beforeEach(() => { - const fifteenMinutesInMilliseconds = 900000; - spyOn(Date, 'now').and.callFake( - () => new Date(delayedJobFixture.scheduled_at).getTime() - fifteenMinutesInMilliseconds, - ); - }); - - it('displays remaining time in tooltip', done => { + it('displays remaining time in tooltip', () => { component = mountComponent(JobComponent, { job: delayedJobFixture, }); - Vue.nextTick() - .then(() => { - expect( - component.$el - .querySelector('.js-pipeline-graph-job-link') - .getAttribute('data-original-title'), - ).toEqual('delayed job - delayed manual action (00:15:00)'); - }) - .then(done) - .catch(done.fail); + expect( + component.$el + .querySelector('.js-pipeline-graph-job-link') + .getAttribute('data-original-title'), + ).toEqual(`delayed job - delayed manual action (${component.remainingTime})`); }); }); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index d6c44f4c976..ea917b36526 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -90,7 +90,7 @@ describe('Pipeline Url Component', () => { expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API'); }); - it('should render latest, yaml invalid and stuck flags when provided', () => { + it('should render latest, yaml invalid, merge request, and stuck flags when provided', () => { const component = new PipelineUrlComponent({ propsData: { pipeline: { @@ -100,6 +100,7 @@ describe('Pipeline Url Component', () => { latest: true, yaml_errors: true, stuck: true, + merge_request: true, }, }, autoDevopsHelpPath: 'foo', @@ -111,6 +112,10 @@ describe('Pipeline Url Component', () => { 'yaml invalid', ); + expect(component.$el.querySelector('.js-pipeline-url-mergerequest').textContent).toContain( + 'merge request', + ); + expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 92ff960277a..67118ac03a5 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,37 +1,30 @@ -import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; import { reposServerResponse } from '../mock_data'; describe('Registry List', () => { + const Component = Vue.extend(registry); let vm; - let Component; + let mock; beforeEach(() => { - Component = Vue.extend(registry); + mock = new MockAdapter(axios); }); afterEach(() => { + mock.restore(); vm.$destroy(); }); describe('with data', () => { - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(reposServerResponse), { - status: 200, - }), - ); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - vm = mountComponent(Component, { endpoint: 'foo' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render a list of repos', done => { @@ -64,9 +57,9 @@ describe('Registry List', () => { Vue.nextTick(() => { vm.$el.querySelector('.js-toggle-repo').click(); Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual( - 'fa fa-chevron-up', - ); + expect( + vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'), + ).toContain('angle-up'); done(); }); }); @@ -76,21 +69,10 @@ describe('Registry List', () => { }); describe('without data', () => { - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify([]), { - status: 200, - }), - ); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - vm = mountComponent(Component, { endpoint: 'foo' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render empty message', done => { @@ -109,21 +91,10 @@ describe('Registry List', () => { }); describe('while loading data', () => { - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(reposServerResponse), { - status: 200, - }), - ); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - vm = mountComponent(Component, { endpoint: 'foo' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render a loading spinner', done => { diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js index 256a242f784..a3f7ff76dc7 100644 --- a/spec/javascripts/registry/components/collapsible_container_spec.js +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -1,14 +1,24 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import collapsibleComponent from '~/registry/components/collapsible_container.vue'; import store from '~/registry/stores'; -import { repoPropsData } from '../mock_data'; +import * as types from '~/registry/stores/mutation_types'; + +import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data'; describe('collapsible registry container', () => { let vm; - let Component; + let mock; + const Component = Vue.extend(collapsibleComponent); beforeEach(() => { - Component = Vue.extend(collapsibleComponent); + mock = new MockAdapter(axios); + + mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {}); + + store.commit(types.SET_REPOS_LIST, reposServerResponse); + vm = new Component({ store, propsData: { @@ -18,24 +28,23 @@ describe('collapsible registry container', () => { }); afterEach(() => { + mock.restore(); vm.$destroy(); }); describe('toggle', () => { it('should be closed by default', () => { expect(vm.$el.querySelector('.container-image-tags')).toBe(null); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-right', - ); + expect(vm.iconName).toEqual('angle-right'); }); it('should be open when user clicks on closed repo', done => { vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { - expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-up', - ); + expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull(); + expect(vm.iconName).toEqual('angle-up'); + done(); }); }); @@ -45,12 +54,12 @@ describe('collapsible registry container', () => { Vue.nextTick(() => { vm.$el.querySelector('.js-toggle-repo').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.container-image-tags')).toBe(null); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-right', - ); - done(); + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.iconName).toEqual('angle-right'); + done(); + }); }); }); }); @@ -58,7 +67,7 @@ describe('collapsible registry container', () => { describe('delete repo', () => { it('should be possible to delete a repo', () => { - expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); + expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull(); }); }); }); diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js index bc4c444655a..c9aa82dba90 100644 --- a/spec/javascripts/registry/stores/actions_spec.js +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -1,42 +1,34 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as actions from '~/registry/stores/actions'; import * as types from '~/registry/stores/mutation_types'; +import state from '~/registry/stores/state'; +import { TEST_HOST } from 'spec/test_constants'; import testAction from '../../helpers/vuex_action_helper'; import { - defaultState, reposServerResponse, registryServerResponse, parsedReposServerResponse, } from '../mock_data'; -Vue.use(VueResource); - describe('Actions Registry Store', () => { - let interceptor; let mockedState; + let mock; beforeEach(() => { - mockedState = defaultState; + mockedState = state(); + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); }); - describe('server requests', () => { - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); + afterEach(() => { + mock.restore(); + }); + describe('server requests', () => { describe('fetchRepos', () => { beforeEach(() => { - interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(reposServerResponse), { - status: 200, - }), - ); - }; - - Vue.http.interceptors.push(interceptor); + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {}); }); it('should set receveived repos', done => { @@ -56,23 +48,15 @@ describe('Actions Registry Store', () => { }); describe('fetchList', () => { + let repo; beforeEach(() => { - interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(registryServerResponse), { - status: 200, - }), - ); - }; + mockedState.repos = parsedReposServerResponse; + [, repo] = mockedState.repos; - Vue.http.interceptors.push(interceptor); + mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {}); }); it('should set received list', done => { - mockedState.repos = parsedReposServerResponse; - - const repo = mockedState.repos[1]; - testAction( actions.fetchList, { repo }, diff --git a/spec/javascripts/releases/components/app_spec.js b/spec/javascripts/releases/components/app_spec.js new file mode 100644 index 00000000000..f30c7685e34 --- /dev/null +++ b/spec/javascripts/releases/components/app_spec.js @@ -0,0 +1,79 @@ +import Vue from 'vue'; +import app from '~/releases/components/app.vue'; +import createStore from '~/releases/store'; +import api from '~/api'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../store/helpers'; +import { releases } from '../mock_data'; + +describe('Releases App ', () => { + const Component = Vue.extend(app); + let store; + let vm; + + const props = { + projectId: 'gitlab-ce', + documentationLink: 'help/releases', + illustrationPath: 'illustration/path', + }; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + resetStore(store); + vm.$destroy(); + }); + + describe('while loading', () => { + beforeEach(() => { + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] })); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders loading icon', done => { + expect(vm.$el.querySelector('.js-loading')).not.toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + + setTimeout(() => { + done(); + }, 0); + }); + }); + + describe('with successful request', () => { + beforeEach(() => { + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases })); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders success state', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); + + done(); + }, 0); + }); + }); + + describe('with empty request', () => { + beforeEach(() => { + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] })); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders empty state', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js new file mode 100644 index 00000000000..19aecbb3636 --- /dev/null +++ b/spec/javascripts/releases/components/release_block_spec.js @@ -0,0 +1,136 @@ +import Vue from 'vue'; +import component from '~/releases/components/release_block.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Release block', () => { + const Component = Vue.extend(component); + + const release = { + name: 'Bionic Beaver', + tag_name: '18.04', + description: '## changelog\n\n* line 1\n* line2', + description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', + author_name: 'Release bot', + author_email: 'release-bot@example.com', + created_at: '2012-05-28T05:00:00-07:00', + commit: { + id: '2695effb5807a22ff3d138d593fd856244e155e7', + short_id: '2695effb', + title: 'Initial commit', + created_at: '2017-07-26T11:08:53.000+02:00', + parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], + message: 'Initial commit', + author_name: 'John Smith', + author_email: 'john@example.com', + authored_date: '2012-05-28T04:42:42-07:00', + committer_name: 'Jack Smith', + committer_email: 'jack@example.com', + committed_date: '2012-05-28T04:42:42-07:00', + author: { + avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', + id: 482476, + name: 'John Doe', + path: '/johndoe', + state: 'active', + status_tooltip_html: null, + username: 'johndoe', + web_url: 'https://gitlab.com/johndoe', + }, + }, + assets: { + count: 6, + sources: [ + { + format: 'zip', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip', + }, + { + format: 'tar.gz', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', + }, + { + format: 'tar.bz2', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', + }, + { + format: 'tar', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', + }, + ], + links: [ + { + name: 'release-18.04.dmg', + url: 'https://my-external-hosting.example.com/scrambled-url/', + external: true, + }, + { + name: 'binary-linux-amd64', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', + external: false, + }, + ], + }, + }; + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { release }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders release name', () => { + expect(vm.$el.textContent).toContain(release.name); + }); + + it('renders commit sha', () => { + expect(vm.$el.textContent).toContain(release.commit.short_id); + }); + + it('renders tag name', () => { + expect(vm.$el.textContent).toContain(release.tag_name); + }); + + it('renders release date', () => { + expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.created_at)); + }); + + it('renders number of assets provided', () => { + expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count); + }); + + it('renders dropdown with the sources', () => { + expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual( + release.assets.sources.length, + ); + + expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual( + release.assets.sources[0].url, + ); + + expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain( + release.assets.sources[0].format, + ); + }); + + it('renders list with the links provided', () => { + expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual( + release.assets.links.length, + ); + + expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual( + release.assets.links[0].url, + ); + + expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain( + release.assets.links[0].name, + ); + }); +}); diff --git a/spec/javascripts/releases/mock_data.js b/spec/javascripts/releases/mock_data.js new file mode 100644 index 00000000000..2855eca1711 --- /dev/null +++ b/spec/javascripts/releases/mock_data.js @@ -0,0 +1,128 @@ +export const release = { + name: 'Bionic Beaver', + tag_name: '18.04', + description: '## changelog\n\n* line 1\n* line2', + description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', + author_name: 'Release bot', + author_email: 'release-bot@example.com', + created_at: '2012-05-28T05:00:00-07:00', + commit: { + id: '2695effb5807a22ff3d138d593fd856244e155e7', + short_id: '2695effb', + title: 'Initial commit', + created_at: '2017-07-26T11:08:53.000+02:00', + parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], + message: 'Initial commit', + author: { + avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', + id: 482476, + name: 'John Doe', + path: '/johndoe', + state: 'active', + status_tooltip_html: null, + username: 'johndoe', + web_url: 'https://gitlab.com/johndoe', + }, + authored_date: '2012-05-28T04:42:42-07:00', + committer_name: 'Jack Smith', + committer_email: 'jack@example.com', + committed_date: '2012-05-28T04:42:42-07:00', + }, + assets: { + count: 6, + sources: [ + { + format: 'zip', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip', + }, + { + format: 'tar.gz', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', + }, + { + format: 'tar.bz2', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', + }, + { + format: 'tar', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', + }, + ], + links: [ + { + name: 'release-18.04.dmg', + url: 'https://my-external-hosting.example.com/scrambled-url/', + external: true, + }, + { + name: 'binary-linux-amd64', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', + external: false, + }, + ], + }, +}; + +export const releases = [ + release, + { + name: 'JoJos Bizarre Adventure', + tag_name: '19.00', + description: '## changelog\n\n* line 1\n* line2', + description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', + author_name: 'Release bot', + author_email: 'release-bot@example.com', + created_at: '2012-05-28T05:00:00-07:00', + commit: { + id: '2695effb5807a22ff3d138d593fd856244e155e7', + short_id: '2695effb', + title: 'Initial commit', + created_at: '2017-07-26T11:08:53.000+02:00', + parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], + message: 'Initial commit', + author: { + avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', + id: 482476, + name: 'John Doe', + path: '/johndoe', + state: 'active', + status_tooltip_html: null, + username: 'johndoe', + web_url: 'https://gitlab.com/johndoe', + }, + authored_date: '2012-05-28T04:42:42-07:00', + committer_name: 'Jack Smith', + committer_email: 'jack@example.com', + committed_date: '2012-05-28T04:42:42-07:00', + }, + assets: { + count: 4, + sources: [ + { + format: 'tar.gz', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', + }, + { + format: 'tar.bz2', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', + }, + { + format: 'tar', + url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', + }, + ], + links: [ + { + name: 'binary-linux-amd64', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', + external: false, + }, + ], + }, + }, +]; diff --git a/spec/javascripts/releases/store/actions_spec.js b/spec/javascripts/releases/store/actions_spec.js new file mode 100644 index 00000000000..6eb8e681be9 --- /dev/null +++ b/spec/javascripts/releases/store/actions_spec.js @@ -0,0 +1,98 @@ +import { + requestReleases, + fetchReleases, + receiveReleasesSuccess, + receiveReleasesError, +} from '~/releases/store/actions'; +import state from '~/releases/store/state'; +import * as types from '~/releases/store/mutation_types'; +import api from '~/api'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { releases } from '../mock_data'; + +describe('Releases State actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('requestReleases', () => { + it('should commit REQUEST_RELEASES mutation', done => { + testAction(requestReleases, null, mockedState, [{ type: types.REQUEST_RELEASES }], [], done); + }); + }); + + describe('fetchReleases', () => { + describe('success', () => { + it('dispatches requestReleases and receiveReleasesSuccess ', done => { + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases })); + + testAction( + fetchReleases, + releases, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: releases, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestReleases and receiveReleasesError ', done => { + spyOn(api, 'releases').and.returnValue(Promise.reject()); + + testAction( + fetchReleases, + null, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + type: 'receiveReleasesError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveReleasesSuccess', () => { + it('should commit RECEIVE_RELEASES_SUCCESS mutation', done => { + testAction( + receiveReleasesSuccess, + releases, + mockedState, + [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: releases }], + [], + done, + ); + }); + }); + + describe('receiveReleasesError', () => { + it('should commit RECEIVE_RELEASES_ERROR mutation', done => { + testAction( + receiveReleasesError, + null, + mockedState, + [{ type: types.RECEIVE_RELEASES_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/releases/store/helpers.js b/spec/javascripts/releases/store/helpers.js new file mode 100644 index 00000000000..e962b254377 --- /dev/null +++ b/spec/javascripts/releases/store/helpers.js @@ -0,0 +1,6 @@ +import state from '~/releases/store/state'; + +// eslint-disable-next-line import/prefer-default-export +export const resetStore = store => { + store.replaceState(state()); +}; diff --git a/spec/javascripts/releases/store/mutations_spec.js b/spec/javascripts/releases/store/mutations_spec.js new file mode 100644 index 00000000000..72b98529fe9 --- /dev/null +++ b/spec/javascripts/releases/store/mutations_spec.js @@ -0,0 +1,47 @@ +import state from '~/releases/store/state'; +import mutations from '~/releases/store/mutations'; +import * as types from '~/releases/store/mutation_types'; +import { releases } from '../mock_data'; + +describe('Releases Store Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('REQUEST_RELEASES', () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_RELEASES](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_RELEASES_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, releases); + }); + + it('sets is loading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('sets hasError to false', () => { + expect(stateCopy.hasError).toEqual(false); + }); + + it('sets data', () => { + expect(stateCopy.releases).toEqual(releases); + }); + }); + + describe('RECEIVE_RELEASES_ERROR', () => { + it('resets data', () => { + mutations[types.RECEIVE_RELEASES_ERROR](stateCopy); + + expect(stateCopy.isLoading).toEqual(false); + expect(stateCopy.releases).toEqual([]); + }); + }); +}); 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_mr_widget/components/mr_widget_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js new file mode 100644 index 00000000000..16c8c939a6f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js @@ -0,0 +1,51 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue'; + +const BODY_HTML = '<div class="test-body">Hello World</div>'; +const FOOTER_HTML = '<div class="test-footer">Goodbye!</div>'; + +describe('MrWidgetContainer', () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetContainer), { + localVue, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('has layout', () => { + factory(); + + expect(wrapper.is('.mr-widget-heading')).toBe(true); + expect(wrapper.contains('.mr-widget-content')).toBe(true); + }); + + it('accepts default slot', () => { + factory({ + slots: { + default: BODY_HTML, + }, + }); + + expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true); + }); + + it('accepts footer slot', () => { + factory({ + slots: { + default: BODY_HTML, + footer: FOOTER_HTML, + }, + }); + + expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true); + expect(wrapper.contains('.test-footer')).toBe(true); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js new file mode 100644 index 00000000000..f7c2376eebf --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js @@ -0,0 +1,30 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const TEST_ICON = 'commit'; + +describe('MrWidgetIcon', () => { + let wrapper; + + beforeEach(() => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetIcon), { + propsData: { + name: TEST_ICON, + }, + sync: false, + localVue, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders icon and container', () => { + expect(wrapper.is('.circle-icon-container')).toBe(true); + expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js new file mode 100644 index 00000000000..e5155573f6f --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js @@ -0,0 +1,90 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue'; +import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import { mockStore } from '../mock_data'; + +describe('MrWidgetPipelineContainer', () => { + let wrapper; + + const factory = (props = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), { + propsData: { + mr: Object.assign({}, mockStore), + ...props, + }, + localVue, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when pre merge', () => { + beforeEach(() => { + factory(); + }); + + it('renders pipeline', () => { + expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true); + expect(wrapper.find(MrWidgetPipeline).props()).toEqual( + jasmine.objectContaining({ + pipeline: mockStore.pipeline, + ciStatus: mockStore.ciStatus, + hasCi: mockStore.hasCI, + sourceBranch: mockStore.sourceBranch, + sourceBranchLink: mockStore.sourceBranchLink, + }), + ); + }); + + it('renders deployments', () => { + const expectedProps = mockStore.deployments.map(dep => + jasmine.objectContaining({ + deployment: dep, + showMetrics: false, + }), + ); + + const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment'); + + expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps); + }); + }); + + describe('when post merge', () => { + beforeEach(() => { + factory({ + isPostMerge: true, + }); + }); + + it('renders pipeline', () => { + expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true); + expect(wrapper.find(MrWidgetPipeline).props()).toEqual( + jasmine.objectContaining({ + pipeline: mockStore.mergePipeline, + ciStatus: mockStore.ciStatus, + hasCi: mockStore.hasCI, + sourceBranch: mockStore.targetBranch, + sourceBranchLink: mockStore.targetBranch, + }), + ); + }); + + it('renders deployments', () => { + const expectedProps = mockStore.postMergeDeployments.map(dep => + jasmine.objectContaining({ + deployment: dep, + showMetrics: true, + }), + ); + + const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment'); + + expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js index 300133dc602..212519743aa 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -114,7 +114,7 @@ describe('Merge request widget rebase component', () => { // Wait for the eventHub to be called .then(vm.$nextTick()) .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 17554c4fe42..072e98fc0e8 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -222,3 +222,16 @@ export default { 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', }; + +export const mockStore = { + pipeline: { id: 0 }, + mergePipeline: { id: 1 }, + targetBranch: 'target-branch', + sourceBranch: 'source-branch', + sourceBranchLink: 'source-branch-link', + deployments: [{ id: 0, name: 'bogus' }, { id: 1, name: 'bogus-docs' }], + postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }], + troubleshootingDocsPath: 'troubleshooting-docs-path', + ciStatus: 'ci-status', + hasCI: true, +}; diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js index 9d34bdd1084..61ef26cd080 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -35,7 +35,7 @@ describe('getStateKey', () => { expect(bound()).toEqual('mergeWhenPipelineSucceeds'); - context.hasSHAChanged = true; + context.isSHAMismatch = true; expect(bound()).toEqual('shaMismatch'); diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js index f5079147f60..c226704694c 100644 --- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js @@ -3,23 +3,30 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mockData from '../mock_data'; describe('MergeRequestStore', () => { - describe('setData', () => { - let store; + let store; - beforeEach(() => { - store = new MergeRequestStore(mockData); - }); + beforeEach(() => { + store = new MergeRequestStore(mockData); + }); - it('should set hasSHAChanged when the diff SHA changes', () => { + describe('setData', () => { + it('should set isSHAMismatch when the diff SHA changes', () => { store.setData({ ...mockData, diff_head_sha: 'a-different-string' }); - expect(store.hasSHAChanged).toBe(true); + expect(store.isSHAMismatch).toBe(true); }); - it('should not set hasSHAChanged when other data changes', () => { + it('should not set isSHAMismatch when other data changes', () => { store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); - expect(store.hasSHAChanged).toBe(false); + expect(store.isSHAMismatch).toBe(false); + }); + + it('should update cached sha after rebasing', () => { + store.setData({ ...mockData, diff_head_sha: 'abc123' }, true); + + expect(store.isSHAMismatch).toBe(false); + expect(store.sha).toBe('abc123'); }); describe('isPipelinePassing', () => { diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js index 67a3a2e08bc..6add6cdac4d 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -68,4 +68,30 @@ describe('DiffViewer', () => { done(); }); }); + + it('renders renamed component', () => { + createComponent({ + diffMode: 'renamed', + newPath: 'test.abc', + newSha: 'ABC', + oldPath: 'testold.abc', + oldSha: 'DEF', + }); + + expect(vm.$el.textContent).toContain('File moved'); + }); + + it('renders mode changed component', () => { + createComponent({ + diffMode: 'mode_changed', + newPath: 'test.abc', + newSha: 'ABC', + oldPath: 'testold.abc', + oldSha: 'DEF', + aMode: '123', + bMode: '321', + }); + + expect(vm.$el.textContent).toContain('File mode changed from 123 to 321'); + }); }); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js new file mode 100644 index 00000000000..c4358f0d9cb --- /dev/null +++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js @@ -0,0 +1,23 @@ +import { shallowMount } from '@vue/test-utils'; +import ModeChanged from '~/vue_shared/components/diff_viewer/viewers/mode_changed.vue'; + +describe('Diff viewer mode changed component', () => { + let vm; + + beforeEach(() => { + vm = shallowMount(ModeChanged, { + propsData: { + aMode: '123', + bMode: '321', + }, + }); + }); + + afterEach(() => { + vm.destroy(); + }); + + it('renders aMode & bMode', () => { + expect(vm.text()).toContain('File mode changed from 123 to 321'); + }); +}); 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/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index abb17440c0e..79e0e756a7a 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -80,7 +80,7 @@ describe('Markdown field component', () => { previewLink.click(); Vue.nextTick(() => { - expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading...'); + expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading…'); done(); }); diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js index 59613faa49f..e733a95288e 100644 --- a/spec/javascripts/vue_shared/components/markdown/header_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js @@ -28,6 +28,7 @@ describe('Markdown field header component', () => { 'Add a numbered list', 'Add a task list', 'Add a table', + 'Insert suggestion', 'Go full screen', ]; const elements = vm.$el.querySelectorAll('.toolbar-btn'); @@ -93,4 +94,18 @@ describe('Markdown field header component', () => { '| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |', ); }); + + it('renders suggestion template', () => { + vm.lineContent = 'Some content'; + + expect(vm.mdSuggestion).toEqual('```suggestion\n{text}\n```'); + }); + + it('does not render suggestion button if `canSuggest` is set to false', () => { + vm.canSuggest = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.qa-suggestion-btn')).toBe(null); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js new file mode 100644 index 00000000000..8187b3204b1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -0,0 +1,69 @@ +import Vue from 'vue'; +import SuggestionDiffHeaderComponent from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; + +const MOCK_DATA = { + canApply: true, + isApplied: false, + helpPagePath: 'path_to_docs', +}; + +describe('Suggestion Diff component', () => { + let vm; + + function createComponent(propsData) { + const Component = Vue.extend(SuggestionDiffHeaderComponent); + + return new Component({ + propsData, + }).$mount(); + } + + beforeEach(done => { + vm = createComponent(MOCK_DATA); + Vue.nextTick(done); + }); + + describe('init', () => { + it('renders a suggestion header', () => { + const header = vm.$el.querySelector('.qa-suggestion-diff-header'); + + expect(header).not.toBeNull(); + expect(header.innerHTML.includes('Suggested change')).toBe(true); + }); + + it('renders an apply button', () => { + const applyBtn = vm.$el.querySelector('.qa-apply-btn'); + + expect(applyBtn).not.toBeNull(); + expect(applyBtn.innerHTML.includes('Apply suggestion')).toBe(true); + }); + + it('does not render an apply button if `canApply` is set to false', () => { + const props = Object.assign(MOCK_DATA, { canApply: false }); + + vm = createComponent(props); + + expect(vm.$el.querySelector('.qa-apply-btn')).toBeNull(); + }); + }); + + describe('applySuggestion', () => { + it('emits when the apply button is clicked', () => { + const props = Object.assign(MOCK_DATA, { canApply: true }); + + vm = createComponent(props); + spyOn(vm, '$emit'); + vm.applySuggestion(); + + expect(vm.$emit).toHaveBeenCalled(); + }); + + it('does not emit when the canApply is set to false', () => { + spyOn(vm, '$emit'); + vm.canApply = false; + vm.applySuggestion(); + + expect(vm.$emit).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js new file mode 100644 index 00000000000..d4ed8f2f7a4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js @@ -0,0 +1,79 @@ +import Vue from 'vue'; +import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue'; + +const MOCK_DATA = { + canApply: true, + newLines: [ + { content: 'Line 1\n', lineNumber: 1 }, + { content: 'Line 2\n', lineNumber: 2 }, + { content: 'Line 3\n', lineNumber: 3 }, + ], + fromLine: 1, + fromContent: 'Old content', + suggestion: { + id: 1, + }, + helpPagePath: 'path_to_docs', +}; + +describe('Suggestion Diff component', () => { + let vm; + + beforeEach(done => { + const Component = Vue.extend(SuggestionDiffComponent); + + vm = new Component({ + propsData: MOCK_DATA, + }).$mount(); + + Vue.nextTick(done); + }); + + describe('init', () => { + it('renders a suggestion header', () => { + expect(vm.$el.querySelector('.qa-suggestion-diff-header')).not.toBeNull(); + }); + + it('renders a diff table', () => { + expect(vm.$el.querySelector('table.md-suggestion-diff')).not.toBeNull(); + }); + + it('renders the oldLineNumber', () => { + const fromLine = vm.$el.querySelector('.qa-old-diff-line-number').innerHTML; + + expect(parseInt(fromLine, 10)).toBe(vm.fromLine); + }); + + it('renders the oldLineContent', () => { + const fromContent = vm.$el.querySelector('.line_content.old').innerHTML; + + expect(fromContent.includes(vm.fromContent)).toBe(true); + }); + + it('renders the contents of newLines', () => { + const newLines = vm.$el.querySelectorAll('.line_holder.new'); + + newLines.forEach((line, i) => { + expect(newLines[i].innerHTML.includes(vm.newLines[i].content)).toBe(true); + }); + }); + + it('renders a line number for each line', () => { + const newLineNumbers = vm.$el.querySelectorAll('.qa-new-diff-line-number'); + + newLineNumbers.forEach((line, i) => { + expect(newLineNumbers[i].innerHTML.includes(vm.newLines[i].lineNumber)).toBe(true); + }); + }); + }); + + describe('applySuggestion', () => { + it('emits apply event when applySuggestion is called', () => { + const callback = () => {}; + spyOn(vm, '$emit'); + vm.applySuggestion(callback); + + expect(vm.$emit).toHaveBeenCalledWith('apply', { suggestionId: vm.suggestion.id, callback }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js new file mode 100644 index 00000000000..ab1b747c360 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue'; + +const MOCK_DATA = { + fromLine: 1, + fromContent: 'Old content', + suggestions: [], + noteHtml: ` + <div class="suggestion"> + <div class="line">Suggestion 1</div> + </div> + + <div class="suggestion"> + <div class="line">Suggestion 2</div> + </div> + `, + isApplied: false, + helpPagePath: 'path_to_docs', +}; + +const generateLine = content => { + const line = document.createElement('div'); + line.className = 'line'; + line.innerHTML = content; + + return line; +}; + +const generateMockLines = () => { + const line1 = generateLine('Line 1'); + const line2 = generateLine('Line 2'); + const line3 = generateLine('Line 3'); + const container = document.createElement('div'); + + container.appendChild(line1); + container.appendChild(line2); + container.appendChild(line3); + + return container; +}; + +describe('Suggestion component', () => { + let vm; + let extractedLines; + let diffTable; + + beforeEach(done => { + const Component = Vue.extend(SuggestionsComponent); + + vm = new Component({ + propsData: MOCK_DATA, + }).$mount(); + + extractedLines = vm.extractNewLines(generateMockLines()); + diffTable = vm.generateDiff(extractedLines).$mount().$el; + + spyOn(vm, 'renderSuggestions'); + vm.renderSuggestions(); + Vue.nextTick(done); + }); + + describe('mounted', () => { + it('renders a flash container', () => { + expect(vm.$el.querySelector('.flash-container')).not.toBeNull(); + }); + + it('renders a container for suggestions', () => { + expect(vm.$refs.container).not.toBeNull(); + }); + + it('renders suggestions', () => { + expect(vm.renderSuggestions).toHaveBeenCalled(); + expect(vm.$el.innerHTML.includes('Suggestion 1')).toBe(true); + expect(vm.$el.innerHTML.includes('Suggestion 2')).toBe(true); + }); + }); + + describe('extractNewLines', () => { + it('extracts suggested lines', () => { + const expectedReturn = [ + { content: 'Line 1\n', lineNumber: 1 }, + { content: 'Line 2\n', lineNumber: 2 }, + { content: 'Line 3\n', lineNumber: 3 }, + ]; + + expect(vm.extractNewLines(generateMockLines())).toEqual(expectedReturn); + }); + + it('increments line number for each extracted line', () => { + expect(extractedLines[0].lineNumber).toEqual(1); + expect(extractedLines[1].lineNumber).toEqual(2); + expect(extractedLines[2].lineNumber).toEqual(3); + }); + + it('returns empty array if no lines are found', () => { + const el = document.createElement('div'); + + expect(vm.extractNewLines(el)).toEqual([]); + }); + }); + + describe('generateDiff', () => { + it('generates a diff table', () => { + expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull(); + }); + + it('generates a diff table that contains contents of `oldLineContent`', () => { + expect(diffTable.innerHTML.includes(vm.fromContent)).toBe(true); + }); + + it('generates a diff table that contains contents the suggested lines', () => { + extractedLines.forEach((line, i) => { + expect(diffTable.innerHTML.includes(extractedLines[i].content)).toBe(true); + }); + }); + + it('generates a diff table with the correct line number for each suggested line', () => { + const lines = diffTable.getElementsByClassName('qa-new-diff-line-number'); + + expect([...lines][0].innerHTML).toBe('1'); + expect([...lines][1].innerHTML).toBe('2'); + expect([...lines][2].innerHTML).toBe('3'); + }); + }); +}); 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..e16ab156679 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js @@ -0,0 +1,146 @@ +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'); + }); + + it('should not encode special characters when we have bio and organization', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Manager & Team Lead'; + testProps.user.organization = 'GitLab'; + + vm = mountComponent(UserPopover, { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }); + + expect(vm.$el.textContent).toContain('Manager & Team Lead 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"'); + }); + }); +}); |