diff options
Diffstat (limited to 'spec/javascripts')
74 files changed, 1650 insertions, 663 deletions
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index b0689fc7cfe..ce5d2022441 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -219,7 +219,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); @@ -244,7 +244,7 @@ describe('AwardsHandler', function() { expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy'); }); - it('handles the special case where "You" is not cleanly comma seperated', function() { + it('handles the special case where "You" is not cleanly comma separated', function() { const awardUrl = awardsHandler.getAwardUrl(); const $votesBlock = $('.js-awards-block').eq(0); const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 037e06cf3b2..2642c8b1bdb 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -18,7 +18,7 @@ describe('Board list component', () => { let mock; let component; - beforeEach((done) => { + beforeEach(done => { const el = document.createElement('div'); document.body.appendChild(el); @@ -62,122 +62,102 @@ describe('Board list component', () => { }); it('renders component', () => { - expect( - component.$el.classList.contains('board-list-component'), - ).toBe(true); + expect(component.$el.classList.contains('board-list-component')).toBe(true); }); - it('renders loading icon', (done) => { + it('renders loading icon', done => { component.loading = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-loading'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-loading')).not.toBeNull(); done(); }); }); it('renders issues', () => { - expect( - component.$el.querySelectorAll('.board-card').length, - ).toBe(1); + expect(component.$el.querySelectorAll('.board-card').length).toBe(1); }); it('sets data attribute with issue id', () => { - expect( - component.$el.querySelector('.board-card').getAttribute('data-issue-id'), - ).toBe('1'); + expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1'); }); - it('shows new issue form', (done) => { + it('shows new issue form', done => { component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('shows new issue form after eventhub event', (done) => { + it('shows new issue form after eventhub event', done => { eventHub.$emit(`hide-issue-form-${component.list.id}`); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull(); - expect( - component.$el.querySelector('.is-smaller'), - ).not.toBeNull(); + expect(component.$el.querySelector('.is-smaller')).not.toBeNull(); done(); }); }); - it('does not show new issue form for closed list', (done) => { + it('does not show new issue form for closed list', done => { component.list.type = 'closed'; component.toggleForm(); Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-new-issue-form'), - ).toBeNull(); + expect(component.$el.querySelector('.board-new-issue-form')).toBeNull(); done(); }); }); - it('shows count list item', (done) => { + it('shows count list item', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count')).not.toBeNull(); - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing all issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing all issues', + ); done(); }); }); - it('sets data attribute with invalid id', (done) => { + it('sets data attribute with invalid id', done => { component.showCount = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), - ).toBe('-1'); + expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe( + '-1', + ); done(); }); }); - it('shows how many more issues to load', (done) => { + it('shows how many more issues to load', done => { component.showCount = true; component.list.issuesSize = 20; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count').textContent.trim(), - ).toBe('Showing 1 of 20 issues'); + expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe( + 'Showing 1 of 20 issues', + ); done(); }); }); - it('loads more issues after scrolling', (done) => { + it('loads more issues after scrolling', done => { spyOn(component.list, 'nextPage'); component.$refs.list.style.height = '100px'; component.$refs.list.style.overflow = 'scroll'; @@ -200,7 +180,9 @@ describe('Board list component', () => { }); it('does not load issues if already loading', () => { - component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); + component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue( + new Promise(() => {}), + ); component.onScroll(); component.onScroll(); @@ -208,14 +190,12 @@ describe('Board list component', () => { expect(component.list.nextPage).toHaveBeenCalledTimes(1); }); - it('shows loading more spinner', (done) => { + it('shows loading more spinner', done => { component.showCount = true; component.list.loadingMore = true; Vue.nextTick(() => { - expect( - component.$el.querySelector('.board-list-count .fa-spinner'), - ).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index d4c53bd5a7d..dee7841c088 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -8,7 +8,7 @@ describe('Board component', () => { let vm; let el; - beforeEach((done) => { + beforeEach(done => { loadFixtures('boards/show.html.raw'); el = document.createElement('div'); @@ -50,56 +50,46 @@ describe('Board component', () => { }); it('board is expandable when list type is backlog', () => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); }); - it('board is expandable when list type is closed', (done) => { + it('board is expandable when list type is closed', done => { vm.list.type = 'closed'; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(true); + expect(vm.$el.classList.contains('is-expandable')).toBe(true); done(); }); }); - it('board is not expandable when list type is label', (done) => { + it('board is not expandable when list type is label', done => { vm.list.type = 'label'; vm.list.isExpandable = false; Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-expandable'), - ).toBe(false); + expect(vm.$el.classList.contains('is-expandable')).toBe(false); done(); }); }); - it('collapses when clicking header', (done) => { + it('collapses when clicking header', done => { vm.$el.querySelector('.board-header').click(); Vue.nextTick(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); }); }); - it('created sets isExpanded to true from localStorage', (done) => { + it('created sets isExpanded to true from localStorage', done => { vm.$el.querySelector('.board-header').click(); return Vue.nextTick() .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); // call created manually vm.$options.created[0].call(vm); @@ -107,11 +97,10 @@ describe('Board component', () => { return Vue.nextTick(); }) .then(() => { - expect( - vm.$el.classList.contains('is-collapsed'), - ).toBe(true); + expect(vm.$el.classList.contains('is-collapsed')).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js new file mode 100644 index 00000000000..9e49330c052 --- /dev/null +++ b/spec/javascripts/boards/components/issue_due_date_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import dateFormat from 'dateformat'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Issue Due Date component', () => { + let vm; + let date; + const Component = Vue.extend(IssueDueDate); + const createComponent = (dueDate = new Date()) => + mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) }); + + beforeEach(() => { + date = new Date(); + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render "Today" if the due date is today', () => { + const timeContainer = vm.$el.querySelector('time'); + + expect(timeContainer.textContent.trim()).toEqual('Today'); + }); + + it('should render "Yesterday" if the due date is yesterday', () => { + date.setDate(date.getDate() - 1); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday'); + }); + + it('should render "Tomorrow" if the due date is one day from now', () => { + date.setDate(date.getDate() + 1); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow'); + }); + + it('should render day of the week if due date is one week away', () => { + date.setDate(date.getDate() + 5); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true)); + }); + + it('should render month and day for other dates', () => { + date.setDate(date.getDate() + 17); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual( + dateFormat(date, 'mmm d', true), + ); + }); + + it('should contain the correct `.text-danger` css class for overdue issue', () => { + date.setDate(date.getDate() - 17); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true); + }); +}); diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js new file mode 100644 index 00000000000..ba65d3287da --- /dev/null +++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Issue Tine Estimate component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(IssueTimeEstimate); + vm = mountComponent(Component, { + estimate: 374460, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the correct time estimate', () => { + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m'); + }); + + it('renders expanded time estimate in tooltip', () => { + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( + '2 weeks 3 days 1 minute', + ); + }); + + it('prevents tooltip xss', done => { + const alertSpy = spyOn(window, 'alert'); + vm.estimate = 'Foo <script>alert("XSS")</script>'; + + vm.$nextTick(() => { + expect(alertSpy).not.toHaveBeenCalled(); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m'); + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m'); + done(); + }); + }); +}); diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 58b7d45d913..6eda5047dd0 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -117,11 +117,9 @@ describe('Issue card component', () => { }); it('sets title', () => { - expect( - component.$el - .querySelector('.board-card-assignee img') - .getAttribute('data-original-title'), - ).toContain(`Assigned to ${user.name}`); + expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain( + `${user.name}`, + ); }); it('sets users path', () => { @@ -154,7 +152,7 @@ describe('Issue card component', () => { it('displays defaults avatar if users avatar is null', () => { expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull(); expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe( - 'default_avatar?width=20', + 'default_avatar?width=24', ); }); }); @@ -163,7 +161,6 @@ describe('Issue card component', () => { describe('multiple assignees', () => { beforeEach(done => { component.issue.assignees = [ - user, new ListAssignee({ id: 2, name: 'user2', @@ -187,11 +184,11 @@ describe('Issue card component', () => { Vue.nextTick(() => done()); }); - it('renders all four assignees', () => { - expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4); + it('renders all three assignees', () => { + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); }); - describe('more than four assignees', () => { + describe('more than three assignees', () => { beforeEach(done => { component.issue.assignees.push( new ListAssignee({ @@ -207,12 +204,12 @@ describe('Issue card component', () => { it('renders more avatar counter', () => { expect( - component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(), ).toEqual('+2'); }); - it('renders three assignees', () => { - expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); + it('renders two assignees', () => { + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2); }); it('renders 99+ avatar counter', done => { @@ -228,7 +225,7 @@ describe('Issue card component', () => { Vue.nextTick(() => { expect( - component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(), ).toEqual('99+'); done(); }); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index a70138c7eee..0e2cc13fa52 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -23,6 +23,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub' }, + knative: { title: 'Knative' }, }, }); }); @@ -46,6 +47,10 @@ describe('Applications', () => { it('renders a row for Jupyter', () => { expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null); }); + + it('renders a row for Knative', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null); + }); }); describe('Ingress application', () => { @@ -63,6 +68,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -86,6 +92,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -105,6 +112,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -123,6 +131,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -139,6 +148,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -155,6 +165,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, + knative: { title: 'Knative', status: 'installed', hostname: '' }, }, }); @@ -171,6 +182,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'not_installable' }, + knative: { title: 'Knative' }, }, }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 4e6ad11cd92..73abf6504c0 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -33,6 +33,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLING, status_reason: 'Cannot connect', }, + { + name: 'knative', + status: APPLICATION_STATUS.INSTALLING, + status_reason: 'Cannot connect', + }, ], }, }, @@ -67,6 +72,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLABLE, status_reason: 'Cannot connect', }, + { + name: 'knative', + status: APPLICATION_STATUS.INSTALLABLE, + status_reason: 'Cannot connect', + }, ], }, }, @@ -77,6 +87,7 @@ const CLUSTERS_MOCK_DATA = { '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {}, + '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {}, }, }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index e0f55a12fca..34ed36afa5b 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -100,6 +100,14 @@ describe('Clusters Store', () => { requestReason: null, hostname: '', }, + knative: { + title: 'Knative', + status: mockResponseData.applications[5].status, + statusReason: mockResponseData.applications[5].status_reason, + requestStatus: null, + requestReason: null, + hostname: null, + }, }, }); }); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 4fc56fd9a27..f6b36e88a5f 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -22,7 +22,7 @@ describe('Commit pipeline status component', () => { Component = Vue.extend(commitPipelineStatus); }); - describe('While polling pipeline data succesfully', () => { + describe('While polling pipeline data successfully', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(() => { @@ -59,14 +59,14 @@ describe('Commit pipeline status component', () => { }); }); - it('contains a ciStatus when the polling is succesful ', done => { + it('contains a ciStatus when the polling is successful ', done => { setTimeout(() => { expect(vm.ciStatus).toEqual(mockCiStatus); done(); }); }); - it('contains a ci-status icon when polling is succesful', done => { + it('contains a ci-status icon when polling is successful', done => { setTimeout(() => { expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); expect(vm.$el.querySelector('.ci-status-icon').classList).toContain( @@ -77,7 +77,7 @@ describe('Commit pipeline status component', () => { }); }); - describe('When polling data was not succesful', () => { + describe('When polling data was not successful', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/dummy/endpoint').reply(502, {}); diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index b797cc44ae7..04c8ab44405 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -72,6 +72,29 @@ describe('Pipelines table in Commits and Merge requests', function() { done(); }, 0); }); + + describe('with pagination', () => { + it('should make an API request when using pagination', done => { + setTimeout(() => { + spyOn(vm, 'updateContent'); + + vm.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + vm.$nextTick(() => { + vm.$el.querySelector('.js-next-button a').click(); + + expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' }); + done(); + }); + }); + }); + }); }); describe('pipeline badge counts', () => { diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index 67f7b569f47..36bd042f3c4 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -1,15 +1,24 @@ import Vue from 'vue'; import DiffContentComponent from '~/diffs/components/diff_content.vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; +import '~/behaviors/markdown/render_gfm'; import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; describe('DiffContent', () => { const Component = Vue.extend(DiffContentComponent); let vm; beforeEach(() => { + const store = createStore(); + store.state.notes.noteableData = { + current_user: { + can_create_note: false, + }, + }; + vm = mountComponentWithStore(Component, { store, props: { @@ -46,21 +55,57 @@ describe('DiffContent', () => { }); describe('image diff', () => { - beforeEach(() => { + beforeEach(done => { vm.diffFile.newPath = GREEN_BOX_IMAGE_URL; vm.diffFile.newSha = 'DEF'; vm.diffFile.oldPath = RED_BOX_IMAGE_URL; vm.diffFile.oldSha = 'ABC'; vm.diffFile.viewPath = ''; + vm.diffFile.discussions = [{ ...discussionsMockData }]; + vm.$store.state.diffs.commentForms.push({ + fileHash: vm.diffFile.fileHash, + x: 10, + y: 20, + width: 100, + height: 200, + }); + + vm.$nextTick(done); }); - it('should have image diff view in place', done => { - vm.$nextTick(() => { - expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0); + it('should have image diff view in place', () => { + expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0); - expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1); + expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1); + }); - done(); + it('renders image diff overlay', () => { + expect(vm.$el.querySelector('.image-diff-overlay')).not.toBe(null); + }); + + it('renders diff file discussions', () => { + expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5); + }); + + describe('handleSaveNote', () => { + it('dispatches handleSaveNote', () => { + spyOn(vm.$store, 'dispatch').and.stub(); + + vm.handleSaveNote('test'); + + expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', { + note: 'test', + formData: { + noteableData: jasmine.anything(), + noteableType: jasmine.anything(), + diffFile: vm.diffFile, + positionType: 'image', + x: 10, + y: 20, + width: 100, + height: 200, + }, + }); }); }); }); diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js index 270f363825f..0bc9da5ad0f 100644 --- a/spec/javascripts/diffs/components/diff_discussions_spec.js +++ b/spec/javascripts/diffs/components/diff_discussions_spec.js @@ -1,24 +1,90 @@ import Vue from 'vue'; import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; -import store from '~/mr_notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import '~/behaviors/markdown/render_gfm'; import discussionsMockData from '../mock_data/diff_discussions'; describe('DiffDiscussions', () => { - let component; + let vm; const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; - beforeEach(() => { - component = createComponentWithStore(Vue.extend(DiffDiscussions), store, { + function createComponent(props = {}) { + const store = createStore(); + + vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, { discussions: getDiscussionsMockData(), + ...props, }).$mount(); + } + + afterEach(() => { + vm.$destroy(); }); describe('template', () => { it('should have notes list', () => { - const { $el } = component; + createComponent(); + + expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5); + }); + }); + + describe('image commenting', () => { + it('renders collapsible discussion button', () => { + createComponent({ shouldCollapseDiscussions: true }); + + expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null); + expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null); + expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain( + 'diff-notes-collapse', + ); + }); + + it('dispatches toggleDiscussion when clicking collapse button', () => { + createComponent({ shouldCollapseDiscussions: true }); + + spyOn(vm.$store, 'dispatch').and.stub(); + + vm.$el.querySelector('.js-diff-notes-toggle').click(); + + expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { + discussionId: vm.discussions[0].id, + }); + }); + + it('renders expand button when discussion is collapsed', done => { + createComponent({ shouldCollapseDiscussions: true }); + + vm.discussions[0].expanded = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1'); + expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain( + 'btn-transparent badge badge-pill', + ); + + done(); + }); + }); + + it('hides discussion when collapsed', done => { + createComponent({ shouldCollapseDiscussions: true }); + + vm.discussions[0].expanded = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none'); + + done(); + }); + }); + + it('renders badge on avatar', () => { + createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] }); - expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5); + expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null); + expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1'); }); }); }); diff --git a/spec/javascripts/diffs/components/image_diff_overlay_spec.js b/spec/javascripts/diffs/components/image_diff_overlay_spec.js new file mode 100644 index 00000000000..d76ab745fe1 --- /dev/null +++ b/spec/javascripts/diffs/components/image_diff_overlay_spec.js @@ -0,0 +1,146 @@ +import Vue from 'vue'; +import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; +import { createStore } from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { imageDiffDiscussions } from '../mock_data/diff_discussions'; + +describe('Diffs image diff overlay component', () => { + const dimensions = { + width: 100, + height: 200, + }; + let Component; + let vm; + + function createComponent(props = {}, extendStore = () => {}) { + const store = createStore(); + + extendStore(store); + + vm = createComponentWithStore(Component, store, { + discussions: [...imageDiffDiscussions], + fileHash: 'ABC', + ...props, + }); + } + + beforeAll(() => { + Component = Vue.extend(ImageDiffOverlay); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders comment badges', () => { + createComponent(); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(2); + }); + + it('renders index of discussion in badge', () => { + createComponent(); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelectorAll('.js-image-badge')[0].textContent.trim()).toBe('1'); + expect(vm.$el.querySelectorAll('.js-image-badge')[1].textContent.trim()).toBe('2'); + }); + + it('renders icon when showCommentIcon is true', () => { + createComponent({ showCommentIcon: true }); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelector('.js-image-badge svg')).not.toBe(null); + }); + + it('sets badge comment positions', () => { + createComponent(); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.left).toBe('10px'); + expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.top).toBe('10px'); + + expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.left).toBe('5px'); + expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.top).toBe('5px'); + }); + + it('renders single badge for discussion object', () => { + createComponent({ + discussions: { + ...imageDiffDiscussions[0], + }, + }); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(1); + }); + + it('dispatches openDiffFileCommentForm when clicking overlay', () => { + createComponent({ canComment: true }); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + spyOn(vm.$store, 'dispatch').and.stub(); + + vm.$el.querySelector('.js-add-image-diff-note-button').click(); + + expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', { + fileHash: 'ABC', + x: 0, + y: 0, + width: 100, + height: 200, + }); + }); + + describe('toggle discussion', () => { + it('disables buttons when shouldToggleDiscussion is false', () => { + createComponent({ shouldToggleDiscussion: false }); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + expect(vm.$el.querySelector('.js-image-badge').hasAttribute('disabled')).toBe(true); + }); + + it('dispatches toggleDiscussion when clicking image badge', () => { + createComponent(); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + + spyOn(vm.$store, 'dispatch').and.stub(); + + vm.$el.querySelector('.js-image-badge').click(); + + expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { discussionId: '1' }); + }); + }); + + describe('comment form', () => { + beforeEach(() => { + createComponent({}, store => { + store.state.diffs.commentForms.push({ + fileHash: 'ABC', + x: 20, + y: 10, + }); + }); + spyOn(vm, 'getImageDimensions').and.returnValue(dimensions); + vm.$mount(); + }); + + it('renders comment form badge', () => { + expect(vm.$el.querySelector('.comment-indicator')).not.toBe(null); + }); + + it('sets comment form badge position', () => { + expect(vm.$el.querySelector('.comment-indicator').style.left).toBe('20px'); + expect(vm.$el.querySelector('.comment-indicator').style.top).toBe('10px'); + }); + }); +}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index 0ad214ea4a4..5ffe5a366ba 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -492,3 +492,24 @@ export default { 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', }; + +export const imageDiffDiscussions = [ + { + id: '1', + position: { + x: 10, + y: 10, + width: 100, + height: 200, + }, + }, + { + id: '2', + position: { + x: 5, + y: 5, + width: 100, + height: 200, + }, + }, +]; diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index d7bc0dbe431..be194ab414f 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -237,4 +237,5 @@ export default { }, }, ], + discussions: [], }; diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index bb623953710..17d0f31bdd3 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -218,6 +218,7 @@ describe('DiffsStoreActions', () => { ], }; const singleDiscussion = { + id: '1', fileHash: 'ABC', line_code: 'ABC_1_1', }; @@ -230,6 +231,7 @@ describe('DiffsStoreActions', () => { { type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, payload: { + id: '1', fileHash: 'ABC', lineCode: 'ABC_1_1', }, diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 807a9e3baf0..9c3a38fd526 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -49,17 +49,17 @@ describe('Diffs Module Getters', () => { }); }); - describe('areAllFilesCollapsed', () => { + describe('hasCollapsedFile', () => { it('returns true when all files are collapsed', () => { localState.diffFiles = [{ collapsed: true }, { collapsed: true }]; - expect(getters.areAllFilesCollapsed(localState)).toEqual(true); + expect(getters.hasCollapsedFile(localState)).toEqual(true); }); - it('returns false when at least one file is not collapsed', () => { + it('returns true when at least one file is collapsed', () => { localState.diffFiles = [{ collapsed: false }, { collapsed: true }]; - expect(getters.areAllFilesCollapsed(localState)).toEqual(false); + expect(getters.hasCollapsedFile(localState)).toEqual(true); }); }); diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js index b7b29190c31..093fec97951 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js @@ -1,23 +1,35 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import { setInput, createForm } from './helper'; +function expectToToggleDisableOnDirtyUpdate(submit, input) { + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + + return setInput(input, `${originalValue} changes`) + .then(() => expect(submit.disabled).toBe(false)) + .then(() => setInput(input, originalValue)) + .then(() => expect(submit.disabled).toBe(true)); +} + describe('DirtySubmitForm', () => { it('disables submit until there are changes', done => { const { form, input, submit } = createForm(); - const originalValue = input.value; new DirtySubmitForm(form); // eslint-disable-line no-new - expect(submit.disabled).toBe(true); + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); + + it('disables submit until there are changes when initializing with a falsy value', done => { + const { form, input, submit } = createForm(); + input.value = ''; + + new DirtySubmitForm(form); // eslint-disable-line no-new - return setInput(input, `${originalValue} changes`) - .then(() => { - expect(submit.disabled).toBe(false); - }) - .then(() => setInput(input, originalValue)) - .then(() => { - expect(submit.disabled).toBe(true); - }) + return expectToToggleDisableOnDirtyUpdate(submit, input) .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js index d71dfe8197e..1f986d49bc7 100644 --- a/spec/javascripts/environments/emtpy_state_spec.js +++ b/spec/javascripts/environments/emtpy_state_spec.js @@ -1,4 +1,3 @@ - import Vue from 'vue'; import emptyState from '~/environments/components/empty_state.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -25,13 +24,13 @@ describe('environments empty state', () => { }); it('renders empty state and new environment button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button').getAttribute('href'), - ).toEqual('foo'); + expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual( + 'foo', + ); }); }); @@ -45,13 +44,11 @@ describe('environments empty state', () => { }); it('renders empty state without new button', () => { - expect( - vm.$el.querySelector('.js-blank-state-title').textContent.trim(), - ).toEqual('You don\'t have any environments right now'); + expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual( + "You don't have any environments right now", + ); - expect( - vm.$el.querySelector('.js-new-environment-button'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull(); }); }); }); diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 223153d4e31..787df757d32 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,15 +1,19 @@ import Vue from 'vue'; -import actionsComp from '~/environments/components/environment_actions.vue'; +import eventHub from '~/environments/event_hub'; +import EnvironmentActions from '~/environments/components/environment_actions.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; -describe('Actions Component', () => { - let ActionsComponent; - let actionsMock; - let component; +describe('EnvironmentActions Component', () => { + const Component = Vue.extend(EnvironmentActions); + let vm; - beforeEach(() => { - ActionsComponent = Vue.extend(actionsComp); + afterEach(() => { + vm.$destroy(); + }); - actionsMock = [ + describe('manual actions', () => { + const actions = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -25,43 +29,89 @@ describe('Actions Component', () => { }, ]; - component = new ActionsComponent({ - propsData: { - actions: actionsMock, - }, - }).$mount(); - }); + beforeEach(() => { + vm = mountComponent(Component, { actions }); + }); + + it('should render a dropdown button with icon and title attribute', () => { + expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined(); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual( + 'Deploy to...', + ); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Deploy to...'); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( + 'Deploy to...', + ); }); - }); - it('should render a dropdown button with icon and title attribute', () => { - expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); - expect( - component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'), - ).toEqual('Deploy to...'); + it('should render a dropdown with the provided list of actions', () => { + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length); + }); - expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( - 'Deploy to...', - ); - }); + it("should render a disabled action when it's not playable", () => { + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); - it('should render a dropdown with the provided list of actions', () => { - expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length); + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); - it("should render a disabled action when it's not playable", () => { - expect( - component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), - ).toEqual('disabled'); + describe('scheduled jobs', () => { + const scheduledJobAction = { + name: 'scheduled action', + playPath: `${TEST_HOST}/scheduled/job/action`, + playable: true, + scheduledAt: '2063-04-05T00:42:00Z', + }; + const expiredJobAction = { + name: 'expired action', + playPath: `${TEST_HOST}/expired/job/action`, + playable: true, + scheduledAt: '2018-10-05T08:23:00Z', + }; + const findDropdownItem = action => { + const buttons = vm.$el.querySelectorAll('.dropdown-menu li button'); + return Array.prototype.find.call(buttons, element => + element.innerText.trim().startsWith(action.name), + ); + }; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + }); + + it('emits postAction event after confirming', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => true); + + findDropdownItem(scheduledJobAction).click(); + + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath }); + }); + + it('does not emit postAction event if confirmation is cancelled', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => false); + + findDropdownItem(scheduledJobAction).click(); - expect( - component.$el - .querySelector('.dropdown-menu li:last-child button') - .classList.contains('disabled'), - ).toEqual(true); + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('displays the remaining time in the dropdown', () => { + expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00'); + }); + + it('displays 00:00:00 for expired jobs in the dropdown', () => { + expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00'); + }); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0b933dda431..7618c2f50ce 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -38,7 +38,9 @@ describe('Environment item', () => { }); it('Should render the number of children in a badge', () => { - expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size); + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain( + mockItem.size, + ); }); }); @@ -68,7 +70,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit: { @@ -84,7 +87,8 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', @@ -121,18 +125,18 @@ describe('Environment item', () => { }); it('should render environment name', () => { - expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name); + expect(component.$el.querySelector('.environment-name').textContent).toContain( + environment.name, + ); }); describe('With deployment', () => { it('should render deployment internal id', () => { - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain(environment.last_deployment.iid); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain( + environment.last_deployment.iid, + ); - expect( - component.$el.querySelector('.deployment-column span').textContent, - ).toContain('#'); + expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#'); }); it('should render last deployment date', () => { @@ -156,56 +160,46 @@ describe('Environment item', () => { describe('With build url', () => { it('Should link to build url provided', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); it('Should render deployable name and id', () => { - expect( - component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.last_deployment.deployable.build_path); + expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual( + environment.last_deployment.deployable.build_path, + ); }); }); describe('With commit information', () => { it('should render commit component', () => { - expect( - component.$el.querySelector('.js-commit-component'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-commit-component')).toBeDefined(); }); }); }); describe('With manual actions', () => { it('Should render actions component', () => { - expect( - component.$el.querySelector('.js-manual-actions-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined(); }); }); describe('With external URL', () => { it('should render external url component', () => { - expect( - component.$el.querySelector('.js-external-url-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-external-url-container')).toBeDefined(); }); }); describe('With stop action', () => { it('Should render stop action component', () => { - expect( - component.$el.querySelector('.js-stop-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined(); }); }); describe('With retry action', () => { it('Should render rollback component', () => { - expect( - component.$el.querySelector('.js-rollback-component-container'), - ).toBeDefined(); + expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined(); }); }); }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index 7edc0ccac0b..e2d81eb454a 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -31,9 +31,9 @@ describe('Environment', () => { mock.restore(); }); - describe('successfull request', () => { + describe('successful request', () => { describe('without environments', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(200, { environments: [] }); component = mountComponent(EnvironmentsComponent, mockData); @@ -44,30 +44,34 @@ describe('Environment', () => { }); it('should render the empty state', () => { - expect( - component.$el.querySelector('.js-new-environment-button').textContent, - ).toContain('New environment'); + expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain( + 'New environment', + ); - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('with paginated environments', () => { - beforeEach((done) => { - mock.onGet(mockData.endpoint).reply(200, { - environments: [environment], - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '1', - 'X-Prev-Page': '', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', - }); + beforeEach(done => { + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: [environment], + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }, + ); component = mountComponent(EnvironmentsComponent, mockData); @@ -78,19 +82,17 @@ describe('Environment', () => { it('should render a table with environments', () => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environment.name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environment.name, + ); }); describe('pagination', () => { it('should render pagination', () => { - expect( - component.$el.querySelectorAll('.gl-pagination li').length, - ).toEqual(5); + expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5); }); - it('should make an API request when page is clicked', (done) => { + it('should make an API request when page is clicked', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); @@ -100,7 +102,7 @@ describe('Environment', () => { }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -114,7 +116,7 @@ describe('Environment', () => { }); describe('unsuccessfull request', () => { - beforeEach((done) => { + beforeEach(done => { mock.onGet(mockData.endpoint).reply(500, {}); component = mountComponent(EnvironmentsComponent, mockData); @@ -125,15 +127,16 @@ describe('Environment', () => { }); it('should render empty state', () => { - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now'); + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain( + "You don't have any environments right now", + ); }); }); describe('expandable folders', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [folder], stopped_count: 0, @@ -154,7 +157,7 @@ describe('Environment', () => { component = mountComponent(EnvironmentsComponent, mockData); }); - it('should open a closed folder', (done) => { + it('should open a closed folder', done => { setTimeout(() => { component.$el.querySelector('.folder-name').click(); @@ -165,7 +168,7 @@ describe('Environment', () => { }, 0); }); - it('should close an opened folder', (done) => { + it('should close an opened folder', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -182,7 +185,7 @@ describe('Environment', () => { }, 0); }); - it('should show children environments and a button to show all environments', (done) => { + it('should show children environments and a button to show all environments', done => { setTimeout(() => { // open folder component.$el.querySelector('.folder-name').click(); @@ -191,7 +194,9 @@ describe('Environment', () => { // wait for next async request setTimeout(() => { expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); - expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); + expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain( + 'Show all', + ); done(); }); }); @@ -201,7 +206,8 @@ describe('Environment', () => { describe('methods', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, + mock.onGet(mockData.endpoint).reply( + 200, { environments: [], stopped_count: 0, @@ -215,8 +221,9 @@ describe('Environment', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '3' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '3' }) .then(() => { expect(component.page).toEqual('3'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 51d4213c38f..7f0a9475d5f 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -30,39 +30,43 @@ describe('Environments Folder View', () => { component.$destroy(); }); - describe('successfull request', () => { + describe('successful request', () => { beforeEach(() => { - mock.onGet(mockData.endpoint).reply(200, { - environments: environmentsList, - stopped_count: 1, - available_count: 0, - }, { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '2', - 'X-Prev-Page': '', - 'X-TOTAL': '20', - 'X-Total-Pages': '10', - }); + mock.onGet(mockData.endpoint).reply( + 200, + { + environments: environmentsList, + stopped_count: 1, + available_count: 0, + }, + { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '20', + 'X-Total-Pages': '10', + }, + ); component = mountComponent(Component, mockData); }); - it('should render a table with environments', (done) => { + it('should render a table with environments', done => { setTimeout(() => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environmentsList[0].name); + expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual( + environmentsList[0].name, + ); done(); }, 0); }); - it('should render available tab with count', (done) => { + it('should render available tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -71,11 +75,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count', (done) => { + it('should render stopped tab with count', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -84,36 +88,37 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render parent folder name', (done) => { + it('should render parent folder name', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-folder-name').textContent.trim(), - ).toContain('Environments / review'); + expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain( + 'Environments / review', + ); done(); }, 0); }); describe('pagination', () => { - it('should render pagination', (done) => { + it('should render pagination', done => { setTimeout(() => { - expect( - component.$el.querySelectorAll('.gl-pagination'), - ).not.toBeNull(); + expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull(); done(); }, 0); }); - it('should make an API request when changing page', (done) => { + it('should make an API request when changing page', done => { spyOn(component, 'updateContent'); setTimeout(() => { component.$el.querySelector('.gl-pagination .js-last-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' }); + expect(component.updateContent).toHaveBeenCalledWith({ + scope: component.scope, + page: '10', + }); done(); }, 0); }); - it('should make an API request when using tabs', (done) => { + it('should make an API request when using tabs', done => { setTimeout(() => { spyOn(component, 'updateContent'); component.$el.querySelector('.js-environments-tab-stopped').click(); @@ -134,20 +139,18 @@ describe('Environments Folder View', () => { component = mountComponent(Component, mockData); }); - it('should not render a table', (done) => { + it('should not render a table', done => { setTimeout(() => { - expect( - component.$el.querySelector('table'), - ).toBe(null); + expect(component.$el.querySelector('table')).toBe(null); done(); }, 0); }); - it('should render available tab with count 0', (done) => { + it('should render available tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-available').textContent, - ).toContain('Available'); + expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain( + 'Available', + ); expect( component.$el.querySelector('.js-environments-tab-available .badge').textContent, @@ -156,11 +159,11 @@ describe('Environments Folder View', () => { }, 0); }); - it('should render stopped tab with count 0', (done) => { + it('should render stopped tab with count 0', done => { setTimeout(() => { - expect( - component.$el.querySelector('.js-environments-tab-stopped').textContent, - ).toContain('Stopped'); + expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain( + 'Stopped', + ); expect( component.$el.querySelector('.js-environments-tab-stopped .badge').textContent, @@ -181,8 +184,9 @@ describe('Environments Folder View', () => { }); describe('updateContent', () => { - it('should set given parameters', (done) => { - component.updateContent({ scope: 'stopped', page: '4' }) + it('should set given parameters', done => { + component + .updateContent({ scope: 'stopped', page: '4' }) .then(() => { expect(component.page).toEqual('4'); expect(component.scope).toEqual('stopped'); diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb index 6d5c6d5334f..82d7a5e394e 100644 --- a/spec/javascripts/fixtures/jobs.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -5,16 +5,24 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') } + let!(:delayed_job) do + create(:ci_build, :scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end render_views before(:all) do clean_frontend_fixtures('builds/') + clean_frontend_fixtures('jobs/') end before do @@ -34,4 +42,15 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do expect(response).to be_success store_frontend_fixture(response, example.description) end + + it 'jobs/delayed.json' do |example| + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: delayed_job.to_param, + format: :json + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end end diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index 9e6f236b687..9754c8a6755 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -25,25 +25,21 @@ describe('Title component', () => { }); it('renders title HTML', () => { - expect( - vm.$el.querySelector('.title').innerHTML.trim(), - ).toBe('Testing <img>'); + expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); }); - it('updates page title when changing titleHtml', (done) => { + it('updates page title when changing titleHtml', done => { spyOn(vm, 'setPageTitle'); vm.titleHtml = 'test'; Vue.nextTick(() => { - expect( - vm.setPageTitle, - ).toHaveBeenCalled(); + expect(vm.setPageTitle).toHaveBeenCalled(); done(); }); }); - it('animates title changes', (done) => { + it('animates title changes', done => { vm.titleHtml = 'test'; Vue.nextTick(() => { @@ -61,14 +57,12 @@ describe('Title component', () => { }); }); - it('updates page title after changing title', (done) => { + it('updates page title after changing title', done => { vm.titleHtml = 'changed'; vm.titleText = 'changed'; Vue.nextTick(() => { - expect( - document.querySelector('title').textContent.trim(), - ).toContain('changed'); + expect(document.querySelector('title').textContent.trim()).toContain('changed'); done(); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 288c06d6615..fcf3780f0ea 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -8,6 +8,7 @@ import { resetStore } from '../store/helpers'; import job from '../mock_data'; describe('Job App ', () => { + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); const Component = Vue.extend(jobApp); let store; let vm; @@ -52,7 +53,7 @@ describe('Job App ', () => { }); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {}); }); @@ -101,7 +102,7 @@ describe('Job App ', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('passed Job #4757 triggered 1 year ago by Root'); + ).toContain('passed Job #4757 triggered 1 year ago by Root'); done(); }, 0); }); @@ -127,7 +128,7 @@ describe('Job App ', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('passed Job #4757 created 3 weeks ago by Root'); + ).toContain('passed Job #4757 created 3 weeks ago by Root'); done(); }, 0); }); @@ -234,7 +235,7 @@ describe('Job App ', () => { ); done(); }, 0); - }) + }); }); it('does not renders stuck block when there are no runners', done => { @@ -420,6 +421,70 @@ describe('Job App ', () => { done(); }, 0); }); + + it('displays remaining time for a delayed job', done => { + const oneHourInMilliseconds = 3600000; + spyOn(Date, 'now').and.callFake( + () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds, + ); + mock.onGet(props.endpoint).replyOnce(200, { ...delayedJobFixture }); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + store.subscribeAction(action => { + if (action.type !== 'receiveJobSuccess') { + return; + } + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull(); + + const title = vm.$el.querySelector('.js-job-empty-state-title'); + + expect(title).toContainText('01:00:00'); + done(); + }) + .catch(done.fail); + }); + }); + }); + }); + + describe('archived job', () => { + beforeEach(() => { + mock.onGet(props.endpoint).reply(200, Object.assign({}, job, { archived: true }), {}); + vm = mountComponentWithStore(Component, { + props, + store, + }); + }); + + it('renders warning about job being archived', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-archived-job ')).not.toBeNull(); + done(); + }, 0); + }); + }); + + describe('non-archived job', () => { + beforeEach(() => { + mock.onGet(props.endpoint).reply(200, job, {}); + vm = mountComponentWithStore(Component, { + props, + store, + }); + }); + + it('does not warning about job being archived', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-archived-job ')).toBeNull(); + done(); + }, 0); }); }); diff --git a/spec/javascripts/jobs/components/job_container_item_spec.js b/spec/javascripts/jobs/components/job_container_item_spec.js index 8588eda19c8..2d108f1ad7f 100644 --- a/spec/javascripts/jobs/components/job_container_item_spec.js +++ b/spec/javascripts/jobs/components/job_container_item_spec.js @@ -4,6 +4,7 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; import job from '../mock_data'; describe('JobContainerItem', () => { + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); const Component = Vue.extend(JobContainerItem); let vm; @@ -70,4 +71,29 @@ describe('JobContainerItem', () => { expect(vm.$el).toHaveSpriteIcon('retry'); }); }); + + describe('for delayed job', () => { + beforeEach(() => { + const remainingMilliseconds = 1337000; + spyOn(Date, 'now').and.callFake( + () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds, + ); + }); + + it('displays remaining time in tooltip', done => { + vm = mountComponent(Component, { + job: delayedJobFixture, + isActive: false, + }); + + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual( + 'delayed job - delayed manual action (00:22:17)', + ); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js new file mode 100644 index 00000000000..48a6b80b365 --- /dev/null +++ b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('DelayedJobMixin', () => { + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); + const dummyComponent = Vue.extend({ + mixins: [delayedJobMixin], + props: { + job: { + type: Object, + required: true, + }, + }, + template: '<div>{{ remainingTime }}</div>', + }); + + let vm; + + beforeEach(() => { + jasmine.clock().install(); + }); + + afterEach(() => { + vm.$destroy(); + jasmine.clock().uninstall(); + }); + + describe('if job is empty object', () => { + beforeEach(() => { + vm = mountComponent(dummyComponent, { + job: {}, + }); + }); + + it('sets remaining time to 00:00:00', () => { + expect(vm.$el.innerText).toBe('00:00:00'); + }); + + describe('after mounting', () => { + beforeEach(done => { + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('doe not update remaining time', () => { + expect(vm.$el.innerText).toBe('00:00:00'); + }); + }); + }); + + describe('if job is delayed job', () => { + let remainingTimeInMilliseconds = 42000; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake( + () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds, + ); + vm = mountComponent(dummyComponent, { + job: delayedJobFixture, + }); + }); + + it('sets remaining time to 00:00:00', () => { + expect(vm.$el.innerText).toBe('00:00:00'); + }); + + describe('after mounting', () => { + beforeEach(done => { + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('sets remaining time', () => { + expect(vm.$el.innerText).toBe('00:00:42'); + }); + + it('updates remaining time', done => { + remainingTimeInMilliseconds = 41000; + jasmine.clock().tick(1000); + + Vue.nextTick() + .then(() => { + expect(vm.$el.innerText).toBe('00:00:41'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); +}); diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index 45130b983e7..77b44995b12 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -68,41 +68,20 @@ describe('Job State actions', () => { describe('hideSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - hideSidebar, - null, - mockedState, - [{ type: types.HIDE_SIDEBAR }], - [], - done, - ); + testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done); }); }); describe('showSidebar', () => { it('should commit HIDE_SIDEBAR mutation', done => { - testAction( - showSidebar, - null, - mockedState, - [{ type: types.SHOW_SIDEBAR }], - [], - done, - ); + testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done); }); }); describe('toggleSidebar', () => { describe('when isSidebarOpen is true', () => { it('should dispatch hideSidebar', done => { - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'hideSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done); }); }); @@ -110,14 +89,7 @@ describe('Job State actions', () => { it('should dispatch showSidebar', done => { mockedState.isSidebarOpen = false; - testAction( - toggleSidebar, - null, - mockedState, - [], - [{ type: 'showSidebar' }], - done, - ); + testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done); }); }); }); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 34e9707eadd..4195d9d3680 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -180,7 +180,7 @@ describe('Job Store Getters', () => { it('returns true', () => { localState.job.runners = { available: true, - online: false + online: false, }; expect(getters.hasRunnersForProject(localState)).toEqual(true); @@ -191,7 +191,7 @@ describe('Job Store Getters', () => { it('returns false', () => { localState.job.runners = { available: false, - online: false + online: false, }; expect(getters.hasRunnersForProject(localState)).toEqual(false); @@ -202,7 +202,7 @@ describe('Job Store Getters', () => { it('returns false', () => { localState.job.runners = { available: false, - online: true + online: true, }; expect(getters.hasRunnersForProject(localState)).toEqual(false); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 514d6ddeae5..0fb90c3b78c 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -35,9 +35,7 @@ describe('common_utils', () => { }); it('should decode params', () => { - expect( - commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0], - ).toBe('label_name[]=test'); + expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); }); it('should remove the question mark from the search params', () => { @@ -49,25 +47,19 @@ describe('common_utils', () => { describe('urlParamsToObject', () => { it('parses path for label with trailing +', () => { - expect( - commonUtils.urlParamsToObject('label_name[]=label%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ label_name: ['label+'], }); }); it('parses path for milestone with trailing +', () => { - expect( - commonUtils.urlParamsToObject('milestone_title=A%2B', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ milestone_title: 'A+', }); }); it('parses path for search terms with spaces', () => { - expect( - commonUtils.urlParamsToObject('search=two+words', {}), - ).toEqual({ + expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({ search: 'two words', }); }); @@ -187,7 +179,11 @@ describe('common_utils', () => { describe('parseQueryStringIntoObject', () => { it('should return object with query parameters', () => { - expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ + scope: 'all', + page: '2', + }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); }); @@ -211,7 +207,9 @@ describe('common_utils', () => { describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); - expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual( + `${window.location.pathname}?page=2`, + ); }); }); @@ -266,21 +264,24 @@ describe('common_utils', () => { }); describe('normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + beforeEach(function() { + this.CLRFHeaders = + 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; spyOn(String.prototype, 'split').and.callThrough(); this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); }); - it('should split by newline', function () { + it('should split by newline', function() { expect(String.prototype.split).toHaveBeenCalledWith('\n'); }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + it('should split by colon+space for each header', function() { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe( + 3, + ); }); - it('should return a normalized headers object', function () { + it('should return a normalized headers object', function() { expect(this.normalizeCRLFHeaders).toEqual({ 'A-HEADER': 'a-value', 'ANOTHER-HEADER': 'ANOTHER-VALUE', @@ -359,67 +360,79 @@ describe('common_utils', () => { spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); }); - it('solves the promise from the callback', (done) => { + it('solves the promise from the callback', done => { const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + commonUtils + .backOff((next, stop) => + new Promise(resolve => { + resolve(expectedResponseValue); + }) + .then(resp => { + stop(resp); + }) + .catch(done.fail), + ) + .then(respBackoff => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }) - ).catch(done.fail)).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + .catch(done.fail); }); - it('catches the rejected promise from the callback ', (done) => { + it('catches the rejected promise from the callback ', done => { const errorMessage = 'Mistakes were made!'; - commonUtils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); - done(); - }); + commonUtils + .backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }) + .then(resp => { + stop(resp); + }) + .catch(err => stop(err)); + }) + .catch(errBackoffResp => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); + }); }); - it('solves the promise correctly after retrying a third time', (done) => { + it('solves the promise correctly after retrying a third time', done => { let numberOfCalls = 1; const expectedResponseValue = 'Success!'; - commonUtils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - ).catch(done.fail)).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + commonUtils + .backOff((next, stop) => + Promise.resolve(expectedResponseValue) + .then(resp => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } + }) + .catch(done.fail), + ) + .then(respBackoff => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }).catch(done.fail); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); + }) + .catch(done.fail); }); - it('rejects the backOff promise after timing out', (done) => { - commonUtils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + it('rejects the backOff promise after timing out', done => { + commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); + done(); + }); }); }); @@ -466,11 +479,14 @@ describe('common_utils', () => { }); describe('createOverlayIcon', () => { - it('should return the favicon with the overlay', (done) => { - commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { - expect(url).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should return the favicon with the overlay', done => { + commonUtils + .createOverlayIcon(faviconDataUrl, overlayDataUrl) + .then(url => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); }); @@ -486,11 +502,16 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should set page favicon to provided favicon overlay', (done) => { - commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { - expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); - done(); - }).catch(done.fail); + it('should set page favicon to provided favicon overlay', done => { + commonUtils + .setFaviconOverlay(overlayDataUrl) + .then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual( + faviconWithOverlayDataUrl, + ); + done(); + }) + .catch(done.fail); }); }); @@ -512,24 +533,24 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should reset favicon in case of error', (done) => { + it('should reset favicon in case of error', done => { mock.onGet(BUILD_URL).replyOnce(500); - commonUtils.setCiStatusFavicon(BUILD_URL) - .catch(() => { - const favicon = document.getElementById('favicon'); + commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => { + const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); - done(); - }); + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); + done(); + }); }); - it('should set page favicon to CI status favicon based on provided status', (done) => { + it('should set page favicon to CI status favicon based on provided status', done => { mock.onGet(BUILD_URL).reply(200, { favicon: overlayDataUrl, }); - commonUtils.setCiStatusFavicon(BUILD_URL) + commonUtils + .setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); @@ -554,11 +575,15 @@ describe('common_utils', () => { }); it('should return the svg for a linked icon', () => { - expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test')).toEqual( + '<svg ><use xlink:href="icons.svg#test" /></svg>', + ); }); it('should set svg className when passed', () => { - expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); + expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual( + '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>', + ); }); }); @@ -578,7 +603,7 @@ describe('common_utils', () => { const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); - Object.keys(convertedObj).forEach((prop) => { + Object.keys(convertedObj).forEach(prop => { expect(snakeRegEx.test(prop)).toBeFalsy(); expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); }); @@ -597,9 +622,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({ snakeKey: { child_snake_key: 'value', }, @@ -614,9 +637,7 @@ describe('common_utils', () => { }, }; - expect( - commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }), - ).toEqual({ + expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ snakeKey: { childSnakeKey: 'value', }, @@ -630,9 +651,7 @@ describe('common_utils', () => { }, ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ { childSnakeKey: 'value', }, @@ -648,9 +667,7 @@ describe('common_utils', () => { ], ]; - expect( - commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }), - ).toEqual([ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ [ { childSnakeKey: 'value', diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index de6b96aab57..bebe76f76c5 100644 --- a/spec/javascripts/lib/utils/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -199,11 +199,11 @@ describe('datefix', () => { expect(datetimeUtility.pad(2)).toEqual('02'); }); - it('should not add a zero when lenght matches the default', () => { + it('should not add a zero when length matches the default', () => { expect(datetimeUtility.pad(12)).toEqual('12'); }); - it('should add a 0 when lenght is smaller than the provided', () => { + it('should add a 0 when length is smaller than the provided', () => { expect(datetimeUtility.pad(12, 3)).toEqual('012'); }); }); @@ -336,6 +336,12 @@ describe('prettyTime methods', () => { expect(timeString).toBe('0m'); }); + + it('should return non-condensed representation of time object', () => { + const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 }; + + expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour'); + }); }); describe('abbreviateTime', () => { diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index a5099a2a3b8..94c6214c86a 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,10 @@ -import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; +import { + formatRelevantDigits, + bytesToKiB, + bytesToMiB, + bytesToGiB, + numberToHumanSize, +} from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index ac3270baef5..92ebfc38722 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -120,7 +120,7 @@ describe('text_utility', () => { }); describe('getFirstCharacterCapitalized', () => { - it('returns the first character captialized, if first character is alphabetic', () => { + it('returns the first character capitalized, if first character is alphabetic', () => { expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L'); expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S'); }); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index a837b71db0b..038bfffd44f 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import GraphFlag from '~/monitoring/components/graph/flag.vue'; import { deploymentData } from '../mock_data'; -const createComponent = (propsData) => { +const createComponent = propsData => { const Component = Vue.extend(GraphFlag); return new Component({ @@ -51,8 +51,7 @@ describe('GraphFlag', () => { it('has a line at the currentXCoordinate', () => { component = createComponent(defaultValuesComponent); - expect(component.$el.style.left) - .toEqual(`${70 + component.currentXCoordinate}px`); + expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`); }); describe('Deployment flag', () => { @@ -62,9 +61,7 @@ describe('GraphFlag', () => { deploymentFlagData, }); - expect( - deploymentFlagComponent.$el.querySelector('.popover-title'), - ).toContainText('Deployed'); + expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed'); }); it('contains the ref when a tag is available', () => { @@ -78,13 +75,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + '1.0', + ); }); it('does not contain the ref when a tag is unavailable', () => { @@ -98,13 +95,13 @@ describe('GraphFlag', () => { }, }); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).toContainText('f5bcd1d9'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( + 'f5bcd1d9', + ); - expect( - deploymentFlagComponent.$el.querySelector('.deploy-meta-content'), - ).not.toContainText('1.0'); + expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText( + '1.0', + ); }); }); diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index 239d7950907..0c16103714a 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import DiffWithNote from '~/notes/components/diff_with_note.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import createStore from '~/notes/stores'; +import { createStore } from '~/mr_notes/stores'; import { mountComponentWithStore } from 'spec/helpers'; const discussionFixture = 'merge_requests/diff_discussion.json'; diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 70dd5bb3be5..9070d968cfd 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -11,13 +11,15 @@ describe('DiscussionFilter component', () => { beforeEach(() => { store = createStore(); - const discussions = [{ - ...discussionMock, - id: discussionMock.id, - notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], - }]; + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + }, + ]; const Component = Vue.extend(DiscussionFilter); - const defaultValue = discussionFiltersMock[0].value; + const selectedValue = discussionFiltersMock[0].value; store.state.discussions = discussions; vm = mountComponentWithStore(Component, { @@ -25,7 +27,7 @@ describe('DiscussionFilter component', () => { store, props: { filters: discussionFiltersMock, - defaultValue, + selectedValue, }, }); }); @@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => { }); it('renders the all filters', () => { - expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length); + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual( + discussionFiltersMock.length, + ); }); it('renders the default selected item', () => { - expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title); + expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual( + discussionFiltersMock[0].title, + ); }); it('updates to the selected item', () => { @@ -57,4 +63,24 @@ describe('DiscussionFilter component', () => { expect(vm.filterDiscussion).not.toHaveBeenCalled(); }); + + it('disables commenting when "Show history only" filter is applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(true); + }); + + it('enables commenting when "Show history only" filter is not applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(false); + }); + + it('renders a dropdown divider for the default filter', () => { + const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + + expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); + }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index d7298cb3483..f6c854e6def 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -55,7 +55,7 @@ describe('issue_note_actions component', () => { expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); }); - it('should be possible to report as abuse', () => { + it('should be possible to report abuse to GitLab', () => { expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 06b30375306..0081f42c330 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -97,7 +97,8 @@ describe('note_app', () => { }); it('should render list of notes', done => { - const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ + const note = + mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[ '/gitlab-org/gitlab-ce/issues/26/discussions.json' ][0].notes[0]; @@ -120,6 +121,13 @@ describe('note_app', () => { ).toEqual('Write a comment or drag your files here…'); }); + it('should not render form when commenting is disabled', () => { + store.state.commentsDisabled = true; + vm = mountComponent(); + + expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null); + }); + it('should render form comment button as disabled', () => { expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( 'disabled', diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index b447e79b0df..81cb3e1f74d 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -3,6 +3,7 @@ import createStore from '~/notes/stores'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; +import mockDiffFile from '../../diffs/mock_data/diff_file'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; @@ -33,9 +34,20 @@ describe('noteable_discussion component', () => { expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); }); + it('should not render discussion header for non diff discussions', () => { + expect(vm.$el.querySelector('.discussion-header')).toBeNull(); + }); + it('should render discussion header', () => { - expect(vm.$el.querySelector('.discussion-header')).not.toBeNull(); - expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length); + const discussion = { ...discussionMock }; + discussion.diff_file = mockDiffFile; + discussion.diff_discussion = true; + const diffDiscussionVm = new Component({ + store, + propsData: { discussion }, + }).$mount(); + + expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull(); }); describe('actions', () => { diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/javascripts/notes/components/toggle_replies_widget_spec.js new file mode 100644 index 00000000000..2ead8cc6e6a --- /dev/null +++ b/spec/javascripts/notes/components/toggle_replies_widget_spec.js @@ -0,0 +1,78 @@ +import Vue from 'vue'; +import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { note } from '../mock_data'; + +const deepCloneObject = obj => JSON.parse(JSON.stringify(obj)); + +describe('toggle replies widget for notes', () => { + let vm; + let ToggleRepliesWidget; + const noteFromOtherUser = deepCloneObject(note); + noteFromOtherUser.author.username = 'fatihacet'; + + const noteFromAnotherUser = deepCloneObject(note); + noteFromAnotherUser.author.username = 'mgreiling'; + noteFromAnotherUser.author.name = 'Mike Greiling'; + + const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser]; + + beforeEach(() => { + ToggleRepliesWidget = Vue.extend(toggleRepliesWidget); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('collapsed state', () => { + beforeEach(() => { + vm = mountComponent(ToggleRepliesWidget, { + replies, + collapsed: true, + }); + }); + + it('should render the collapsed', () => { + const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' '); + + expect(vm.$el.classList.contains('collapsed')).toEqual(true); + expect(vm.$el.querySelectorAll('.user-avatar-link').length).toEqual(3); + expect(vm.$el.querySelector('time')).not.toBeNull(); + expect(vmTextContent).toContain('5 replies'); + expect(vmTextContent).toContain(`Last reply by ${noteFromAnotherUser.author.name}`); + }); + + it('should emit toggle event when the replies text clicked', () => { + const spy = spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-replies-text').click(); + + expect(spy).toHaveBeenCalledWith('toggle'); + }); + }); + + describe('expanded state', () => { + beforeEach(() => { + vm = mountComponent(ToggleRepliesWidget, { + replies, + collapsed: false, + }); + }); + + it('should render expanded state', () => { + const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' '); + + expect(vm.$el.querySelector('.collapse-replies-btn')).not.toBeNull(); + expect(vmTextContent).toContain('Collapse replies'); + }); + + it('should emit toggle event when the collapse replies text called', () => { + const spy = spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-collapse-replies').click(); + + expect(spy).toHaveBeenCalledWith('toggle'); + }); + }); +}); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index f4643fd55ed..0c0bc45b201 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -509,4 +509,17 @@ describe('Actions Notes Store', () => { expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated'); }); }); + + describe('setCommentsDisabled', () => { + it('should set comments disabled state', done => { + testAction( + actions.setCommentsDisabled, + true, + null, + [{ type: 'DISABLE_COMMENTS', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 9d652ba9f1e..461de5a3106 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -78,7 +78,7 @@ describe('Notes Store mutations', () => { }); describe('COLLAPSE_DISCUSSION', () => { - it('should collpase an expanded discussion', () => { + it('should collapse an expanded discussion', () => { const discussion = Object.assign({}, discussionMock, { expanded: true }); const state = { @@ -427,4 +427,14 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].expanded).toBe(true); }); }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index e21dca45fa1..f12950b8fce 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,7 +24,7 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); }); - it('should render emtpy state information', () => { + it('should render empty state information', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 027066e1d4d..3d2232ff239 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -50,7 +50,7 @@ describe('pipeline graph action component', () => { }); describe('on click', () => { - it('emits `pipelineActionRequestComplete` after a successfull request', done => { + it('emits `pipelineActionRequestComplete` after a successful request', done => { spyOn(component, '$emit'); component.$el.click(); diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index b6fa4272c8b..96a2d5f62fa 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -40,7 +40,9 @@ describe('graph component', () => { ).toEqual(true); expect( - component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), + component.$el + .querySelector('.stage-column:nth-child(2) .build:nth-child(1)') + .classList.contains('left-connector'), ).toEqual(true); expect(component.$el.querySelector('loading-icon')).toBe(null); @@ -56,7 +58,9 @@ describe('graph component', () => { pipeline: graphJSON, }); - expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + expect( + component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(), + ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); }); }); }); diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js index 7cbcdc791e7..41b614cc95e 100644 --- a/spec/javascripts/pipelines/graph/job_item_spec.js +++ b/spec/javascripts/pipelines/graph/job_item_spec.js @@ -6,6 +6,7 @@ describe('pipeline graph job item', () => { const JobComponent = Vue.extend(JobItem); let component; + const delayedJobFixture = getJSONFixture('jobs/delayed.json'); const mockJob = { id: 4256, name: 'test', @@ -167,4 +168,30 @@ describe('pipeline graph job item', () => { expect(component.$el.querySelector(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 => { + 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); + }); + }); }); diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js index 473a062fc40..556a0976b29 100644 --- a/spec/javascripts/pipelines/header_component_spec.js +++ b/spec/javascripts/pipelines/header_component_spec.js @@ -51,7 +51,7 @@ describe('Pipeline details header', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo'); + ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo'); }); describe('action buttons', () => { diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index c9011b403b7..d6c44f4c976 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => { }).$mount(); const image = component.$el.querySelector('.js-pipeline-url-user img'); + const tooltip = component.$el.querySelector( + '.js-pipeline-url-user .js-user-avatar-image-toolip', + ); expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual( mockData.pipeline.user.web_url, ); - expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name); + expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name); expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 37908153e0e..97ded16db69 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -372,7 +372,7 @@ describe('Pipelines', () => { }); }); - describe('successfull request', () => { + describe('successful request', () => { describe('with pipelines', () => { beforeEach(() => { mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); @@ -667,7 +667,7 @@ describe('Pipelines', () => { }); }); - it('returns false when state is emtpy state', done => { + it('returns false when state is empty state', done => { vm.isLoading = false; vm.hasMadeRequest = true; vm.hasGitlabCi = false; diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 506d01f5ec1..4c575536f0e 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => { expect( component.$el - .querySelector('.table-section:nth-child(2) img') - .getAttribute('data-original-title'), + .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip') + .textContent.trim(), ).toEqual(pipeline.user.name); }); }); @@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => { const commitAuthorLink = commitAuthorElement.getAttribute('href'); const commitAuthorName = commitAuthorElement - .querySelector('img.avatar') - .getAttribute('data-original-title'); + .querySelector('.js-user-avatar-image-toolip') + .textContent.trim(); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index a3caaeb44dc..3c8b8032de8 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -40,7 +40,7 @@ describe('Pipelines stage component', () => { expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown'); }); - describe('with successfull request', () => { + describe('with successful request', () => { beforeEach(() => { mock.onGet('path.json').reply(200, stageReply); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index f58515daa4f..69767d9cf1c 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => { it('renders resolved failures', done => { setTimeout(() => { - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[0].name, ); - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[1].name, ); done(); diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index eb7307605d7..b02af8baaec 100644 --- a/spec/javascripts/reports/components/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -120,7 +120,7 @@ describe('Report section', () => { 'Code quality improved on 1 point and degraded on 1 point', ); - expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual( + expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual( resolvedIssues.length, ); }); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index e7f8f4f9936..eced4925489 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -78,9 +78,7 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: false, }, }).$mount(); @@ -90,7 +88,10 @@ describe('Assignee component', () => { expect(collapsed.childElementCount).toEqual(1); expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar); - expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`); + expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual( + `${UsersMock.user.name}'s avatar`, + ); + expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); }); @@ -98,34 +99,38 @@ describe('Assignee component', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); expect(component.$el.querySelector('.author-link')).not.toBeNull(); // The image - expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar); + expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual( + UsersMock.user.avatar, + ); // Author name - expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name); + expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual( + UsersMock.user.name, + ); // Username - expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`); + expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual( + `@${UsersMock.user.username}`, + ); }); it('has the root url present in the assigneeUrl method', () => { component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000/', - users: [ - UsersMock.user, - ], + users: [UsersMock.user], editable: true, }, }).$mount(); - expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1); + expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual( + -1, + ); }); }); @@ -147,13 +152,19 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar); - expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`); + expect(second.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[1].name}'s avatar`, + ); + expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name); }); @@ -174,7 +185,10 @@ describe('Assignee component', () => { const first = collapsed.children[0]; expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar); - expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`); + expect(first.querySelector('.avatar').getAttribute('alt')).toEqual( + `${users[0].name}'s avatar`, + ); + expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name); const second = collapsed.children[1]; @@ -196,7 +210,7 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); - it('Shows the "show-less" assignees label', (done) => { + it('Shows the "show-less" assignees label', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -206,21 +220,26 @@ describe('Assignee component', () => { }, }).$mount(); - expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount); + expect(component.$el.querySelectorAll('.user-item').length).toEqual( + component.defaultRenderCount, + ); + expect(component.$el.querySelector('.user-list-more')).not.toBe(null); const usersLabelExpectation = users.length - component.defaultRenderCount; - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .not.toBe(`+${usersLabelExpectation} more`); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe( + `+${usersLabelExpectation} more`, + ); component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); - it('Shows the "show-less" when "n+ more " label is clicked', (done) => { + it('Shows the "show-less" when "n+ more " label is clicked', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { @@ -232,8 +251,9 @@ describe('Assignee component', () => { component.$el.querySelector('.user-list-more .btn-link').click(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); @@ -264,16 +284,18 @@ describe('Assignee component', () => { }); it('shows "+1 more" label', () => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('+ 1 more'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '+ 1 more', + ); }); - it('shows "show less" label', (done) => { + it('shows "show less" label', done => { component.toggleShowLess(); Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()) - .toBe('- show less'); + expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( + '- show less', + ); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index 3d44af11153..2f1bd00fa10 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -242,6 +242,10 @@ describe('Deployment component', () => { it('renders information about running deployment', () => { expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); }); + + it('renders disabled stop button', () => { + expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled'); + }); }); describe('success', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index ea8007d2029..d905bbe4040 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -22,6 +22,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, ciStatus: 'success', hasCi: true, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(true); @@ -30,6 +31,7 @@ describe('MRWidgetPipeline', () => { it('should return false when there is no pipeline', () => { vm = mountComponent(Component, { pipeline: {}, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(false); @@ -42,6 +44,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(false); @@ -52,6 +55,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(true); @@ -65,11 +69,12 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toEqual('Could not connect to the CI server. Please check your settings and try again'); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.', + ); }); describe('with a pipeline', () => { @@ -78,38 +83,41 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status and commit id', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link').textContent.trim(), - ).toEqual(mockData.pipeline.commit.short_id); + expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual( + mockData.pipeline.commit.short_id, + ); - expect( - vm.$el.querySelector('.js-commit-link').getAttribute('href'), - ).toEqual(mockData.pipeline.commit.commit_path); + expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual( + mockData.pipeline.commit.commit_path, + ); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -122,34 +130,35 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); it('should render pipeline ID', () => { - expect( - vm.$el.querySelector('.pipeline-id').textContent.trim(), - ).toEqual(`#${mockData.pipeline.id}`); + expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual( + `#${mockData.pipeline.id}`, + ); }); it('should render pipeline status', () => { - expect( - vm.$el.querySelector('.media-body').textContent.trim(), - ).toContain(mockData.pipeline.details.status.label); + expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( + mockData.pipeline.details.status.label, + ); - expect( - vm.$el.querySelector('.js-commit-link'), - ).toBeNull(); + expect(vm.$el.querySelector('.js-commit-link')).toBeNull(); }); it('should render pipeline graph', () => { expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined(); - expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length); + expect(vm.$el.querySelectorAll('.stage-container').length).toEqual( + mockData.pipeline.details.stages.length, + ); }); it('should render coverage information', () => { - expect( - vm.$el.querySelector('.media-body').textContent, - ).toContain(`Coverage ${mockData.pipeline.coverage}`); + expect(vm.$el.querySelector('.media-body').textContent).toContain( + `Coverage ${mockData.pipeline.coverage}`, + ); }); }); @@ -162,11 +171,10 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); - expect( - vm.$el.querySelector('.media-body').textContent, - ).not.toContain('Coverage'); + expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage'); }); }); @@ -179,6 +187,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index d68342635ef..da5cb752c6f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -69,7 +69,7 @@ describe('MRWidgetMerged', () => { expect(vm.shouldShowRemoveSourceBranch).toEqual(true); }); - it('returns false wehn sourceBranchRemoved is true', () => { + it('returns false when sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; expect(vm.shouldShowRemoveSourceBranch).toEqual(false); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 7fd1a2350f7..17554c4fe42 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -218,5 +218,7 @@ export default { diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', - merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + merge_commit_path: + 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + troubleshooting_docs_path: 'help', }; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 27b6c91e154..09fbe87b27e 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -454,7 +454,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, - status: 'success' + status: 'success', }; beforeEach(done => { @@ -607,33 +607,36 @@ describe('mrWidgetOptions', () => { describe('with post merge deployments', () => { beforeEach(done => { - vm.mr.postMergeDeployments = [{ - id: 15, - name: 'review/diplo', - url: '/root/acets-review-apps/environments/15', - stop_url: '/root/acets-review-apps/environments/15/stop', - metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', - external_url: 'http://diplo.', - external_url_formatted: 'diplo.', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], - status: 'success' - }]; + vm.mr.postMergeDeployments = [ + { + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: + 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + status: 'success', + }, + ]; vm.$nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js index 2f7ea077b54..fd17349d48f 100644 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -27,8 +27,6 @@ describe('clipboard button', () => { it('should have a tooltip with default values', () => { expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); - expect(vm.$el.getAttribute('data-placement')).toEqual('top'); - expect(vm.$el.getAttribute('data-container')).toEqual(null); }); it('should render provided classname', () => { diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 97dacec1fce..18fcdf7ede1 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -98,8 +98,8 @@ describe('Commit component', () => { it('Should render the author avatar with title and alt attributes', () => { expect( component.$el - .querySelector('.commit-title .avatar-image-container img') - .getAttribute('data-original-title'), + .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip') + .textContent.trim(), ).toContain(props.author.username); expect( diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js index e2c34508b0d..4da8c6196b1 100644 --- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js @@ -47,7 +47,7 @@ describe('ContentViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); done(); }); 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 fcd231ec693..67a3a2e08bc 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 @@ -30,11 +30,11 @@ describe('DiffViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe( `//raw/DEF/${RED_BOX_IMAGE_URL}`, ); - expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe( `//raw/ABC/${GREEN_BOX_IMAGE_URL}`, ); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js index 380effdb669..2d3e178d249 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js @@ -52,13 +52,9 @@ describe('ImageDiffViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( - GREEN_BOX_IMAGE_URL, - ); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( - RED_BOX_IMAGE_URL, - ); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( @@ -81,9 +77,7 @@ describe('ImageDiffViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( - GREEN_BOX_IMAGE_URL, - ); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); done(); }); @@ -97,9 +91,7 @@ describe('ImageDiffViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( - RED_BOX_IMAGE_URL, - ); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); done(); }); diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js index b71cb36ecf6..b84b5ae67a8 100644 --- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js +++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js @@ -41,7 +41,7 @@ describe('Filtered search dropdown', () => { }); }); - describe('when visible number is bigger than the items lenght', () => { + describe('when visible number is bigger than the items length', () => { beforeEach(() => { vm = mountComponent(Component, { items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }], diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 3bf497bc00b..7a741bdc067 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -73,7 +73,7 @@ describe('Header CI Component', () => { }); it('should render user icon and name', () => { - expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name); }); it('should render provided actions', () => { diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 3483b7d387d..c507a97d37e 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('toggleCollapse events', () => { - beforeEach((done) => { + beforeEach(done => { spyOn(vm, 'toggleSidebar'); vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); @@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate and maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); @@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('minDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.minDate = new Date('07/17/2016'); Vue.nextTick(done); }); @@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => { }); describe('maxDate', () => { - beforeEach((done) => { + beforeEach(done => { vm.maxDate = new Date('07/17/2017'); Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js index 1581f4e3eb1..805ba7b9947 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); }); - it('should render date-picker when editing', (done) => { + it('should render date-picker when editing', done => { vm.editing = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.pika-label')).toBeDefined(); @@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => { }); describe('editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.editable = true; Vue.nextTick(done); }); @@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => { expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); }); - it('should enable editing when edit button is clicked', (done) => { + it('should enable editing when edit button is clicked', done => { vm.isLoading = false; Vue.nextTick(() => { vm.$el.querySelector('.title .btn-blank').click(); @@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => { }); }); - it('should render date if selectedDate', (done) => { + it('should render date if selectedDate', done => { vm.selectedDate = new Date('07/07/2017'); Vue.nextTick(() => { expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); @@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => { }); describe('selectedDate and editable', () => { - beforeEach((done) => { + beforeEach(done => { vm.selectedDate = new Date('07/07/2017'); vm.editable = true; Vue.nextTick(done); @@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => { }); describe('showToggleSidebar', () => { - beforeEach((done) => { + beforeEach(done => { vm.showToggleSidebar = true; Vue.nextTick(done); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 9a691116cf8..804b33422bd 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => { const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + expect(vmMoreLabels.labelsList).toBe( + 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more', + ); vmMoreLabels.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js new file mode 100644 index 00000000000..e723fead65e --- /dev/null +++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Toggle Button', () => { + let vm; + + const createComponent = ({ length, remain }) => { + const smartListProperties = { + rtag: 'section', + wtag: 'ul', + wclass: 'test-class', + // Size in pixels does not matter for our tests here + size: 35, + length, + remain, + }; + + const Component = Vue.extend({ + components: { + SmartVirtualScrollList, + }, + smartListProperties, + items: Array(length).fill(1), + template: ` + <smart-virtual-scroll-list v-bind="$options.smartListProperties"> + <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li> + </smart-virtual-scroll-list>`, + }); + + return mountComponent(Component); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('if the list is shorter than the maximum shown elements', () => { + const listLength = 10; + + beforeEach(() => { + vm = createComponent({ length: listLength, remain: 20 }); + }); + + it('renders without the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).not.toContain('js-virtual-list'); + expect(vm.$el.classList).toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders all children list elements', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(listLength); + }); + }); + + describe('if the list is longer than the maximum shown elements', () => { + const maxItemsShown = 20; + + beforeEach(() => { + vm = createComponent({ length: 1000, remain: maxItemsShown }); + }); + + it('uses the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).toContain('js-virtual-list'); + expect(vm.$el.classList).not.toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders at max twice the maximum shown elements', () => { + expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown); + }); + }); +}); 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 dc7652c77f7..5c4aa7cf844 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 @@ -1,7 +1,7 @@ import Vue from 'vue'; import { placeholderImage } from '~/lazy_loader'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; const DEFAULT_PROPS = { size: 99, @@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() { }); it('should have <img> as a child element', function() { - expect(vm.$el.tagName).toBe('IMG'); - expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); - }); - - it('should properly compute tooltipContainer', function() { - expect(vm.tooltipContainer).toBe('body'); - }); + const imageElement = vm.$el.querySelector('img'); - it('should properly render tooltipContainer', function() { - expect(vm.$el.getAttribute('data-container')).toBe('body'); + expect(imageElement).not.toBe(null); + expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); }); it('should properly compute avatarSizeClass', function() { @@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() { }); it('should properly render img css', function() { - const { classList } = vm.$el; + const { classList } = vm.$el.querySelector('img'); const containsAvatar = classList.contains('avatar'); const containsSizeClass = classList.contains('s99'); const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses); @@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() { }); it('should add lazy attributes', function() { - const { classList } = vm.$el; - const lazyClass = classList.contains('lazy'); + const imageElement = vm.$el.querySelector('img'); + const lazyClass = imageElement.classList.contains('lazy'); expect(lazyClass).toBe(true); - expect(vm.$el.getAttribute('src')).toBe(placeholderImage); - expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('src')).toBe(placeholderImage); + expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + }); + }); + + describe('dynamic tooltip content', () => { + const props = DEFAULT_PROPS; + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount(); + }); + + it('renders the tooltip slot', () => { + expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null); + }); + + it('renders the tooltip content', () => { + expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain( + slots.default[0], + ); + }); + + it('does not render tooltip data attributes for on avatar image', () => { + const avatarImg = vm.$el.querySelector('img'); + + expect(avatarImg.dataset.originalTitle).not.toBeDefined(); + expect(avatarImg.dataset.placement).not.toBeDefined(); + expect(avatarImg.dataset.container).not.toBeDefined(); }); }); }); 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 50b8d49d4bd..0151ad23ba2 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 @@ -44,7 +44,7 @@ describe('User Avatar Link Component', function() { expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); }); - it('should return neccessary props as defined', function() { + it('should return necessary props as defined', function() { _.each(this.propsData, (val, key) => { expect(this.userAvatarLink[key]).toBeDefined(); }); @@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() { it('should only render image tag in link', function() { const childElements = this.userAvatarLink.$el.childNodes; - expect(childElements[0].tagName).toBe('IMG'); + expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null'); // Vue will render the hidden component as <!----> expect(childElements[1].tagName).toBeUndefined(); }); it('should render avatar image tooltip', function() { - expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual( - this.propsData.tooltipText, - ); + expect(this.userAvatarLink.shouldShowUsername).toBe(false); + expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText); }); }); describe('username', function() { it('should not render avatar image tooltip', function() { - expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(), + ).toEqual(''); }); it('should render username prop in <span>', function() { - expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual( - this.propsData.username, - ); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(), + ).toEqual(this.propsData.username); }); it('should render text tooltip for <span>', function() { - expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual( - this.propsData.tooltipText, - ); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset + .originalTitle, + ).toEqual(this.propsData.tooltipText); }); it('should render text tooltip placement for <span>', function() { expect( - this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'), + this.userAvatarLink.$el + .querySelector('.js-user-avatar-link-username') + .getAttribute('tooltip-placement'), ).toEqual(this.propsData.tooltipPlacement); }); }); |