diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-06-06 12:07:37 +0300 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-06-06 12:07:37 +0300 |
commit | bb6b73cf3c64a6f963e6afc657cab937db46564b (patch) | |
tree | 2d934aa9e94adf7952e18ca1ba46c05a3022669a /spec | |
parent | 4cfe9209106d4697cd8836039e93e0c74708d811 (diff) | |
parent | ec37b1b20c09d3a5b5a0e2cd0b03311ec95d7c68 (diff) |
Merge branch 'ide-jobs-log' into 'master'
Show job logs in web IDE
Closes #46245
See merge request gitlab-org/gitlab-ce!19279
Diffstat (limited to 'spec')
7 files changed, 465 insertions, 0 deletions
diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/javascripts/ide/components/jobs/detail/description_spec.js new file mode 100644 index 00000000000..9b715a41499 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/description_spec.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; +import Description from '~/ide/components/jobs/detail/description.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../../mock_data'; + +describe('IDE job description', () => { + const Component = Vue.extend(Description); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + job: jobs[0], + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders job details', () => { + expect(vm.$el.textContent).toContain('#1'); + expect(vm.$el.textContent).toContain('test'); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ci-status-icon .ic-status_passed_borderless')).not.toBe(null); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js new file mode 100644 index 00000000000..fff382a107f --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('IDE job log scroll button', () => { + const Component = Vue.extend(ScrollButton); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + direction: 'up', + disabled: false, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('iconName', () => { + ['up', 'down'].forEach(direction => { + it(`returns icon name for ${direction}`, () => { + vm.direction = direction; + + expect(vm.iconName).toBe(`scroll_${direction}`); + }); + }); + }); + + describe('tooltipTitle', () => { + it('returns title for up', () => { + expect(vm.tooltipTitle).toBe('Scroll to top'); + }); + + it('returns title for down', () => { + vm.direction = 'down'; + + expect(vm.tooltipTitle).toBe('Scroll to bottom'); + }); + }); + + it('emits click event on click', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.btn-scroll').click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('disables button when disabled is true', done => { + vm.disabled = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail_spec.js b/spec/javascripts/ide/components/jobs/detail_spec.js new file mode 100644 index 00000000000..641ba06f653 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail_spec.js @@ -0,0 +1,180 @@ +import Vue from 'vue'; +import JobDetail from '~/ide/components/jobs/detail.vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../mock_data'; + +describe('IDE jobs detail view', () => { + const Component = Vue.extend(JobDetail); + let vm; + + beforeEach(() => { + const store = createStore(); + + store.state.pipelines.detailJob = { + ...jobs[0], + isLoading: true, + output: 'testing', + rawPath: `${gl.TEST_HOST}/raw`, + }; + + vm = createComponentWithStore(Component, store); + + spyOn(vm, 'fetchJobTrace').and.returnValue(Promise.resolve()); + + vm = vm.$mount(); + + spyOn(vm.$refs.buildTrace, 'scrollTo'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('calls fetchJobTrace on mount', () => { + expect(vm.fetchJobTrace).toHaveBeenCalled(); + }); + + it('scrolls to bottom on mount', done => { + setTimeout(() => { + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled(); + + done(); + }); + }); + + it('renders job output', () => { + expect(vm.$el.querySelector('.bash').textContent).toContain('testing'); + }); + + it('renders empty message output', done => { + vm.$store.state.pipelines.detailJob.output = ''; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); + + done(); + }); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null); + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe(''); + }); + + it('hide loading icon when isLoading is false', done => { + vm.$store.state.pipelines.detailJob.isLoading = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); + + done(); + }); + }); + + it('resets detailJob when clicking header button', () => { + spyOn(vm, 'setDetailJob'); + + vm.$el.querySelector('.btn').click(); + + expect(vm.setDetailJob).toHaveBeenCalledWith(null); + }); + + it('renders raw path link', () => { + expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe( + `${gl.TEST_HOST}/raw`, + ); + }); + + describe('scroll buttons', () => { + it('triggers scrollDown when clicking down button', done => { + spyOn(vm, 'scrollDown'); + + vm.$el.querySelectorAll('.btn-scroll')[1].click(); + + vm.$nextTick(() => { + expect(vm.scrollDown).toHaveBeenCalled(); + + done(); + }); + }); + + it('triggers scrollUp when clicking up button', done => { + spyOn(vm, 'scrollUp'); + + vm.scrollPos = 1; + + vm + .$nextTick() + .then(() => vm.$el.querySelector('.btn-scroll').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(vm.scrollUp).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('scrollDown', () => { + it('scrolls build trace to bottom', () => { + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(1000); + + vm.scrollDown(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000); + }); + }); + + describe('scrollUp', () => { + it('scrolls build trace to top', () => { + vm.scrollUp(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('scrollBuildLog', () => { + beforeEach(() => { + spyOnProperty(vm.$refs.buildTrace, 'offsetHeight').and.returnValue(100); + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(200); + }); + + it('sets scrollPos to bottom when at the bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(100); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(1); + + done(); + }); + }); + + it('sets scrollPos to top when at the top', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(0); + vm.scrollPos = 1; + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(0); + + done(); + }); + }); + + it('resets scrollPos when not at top or bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(10); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(''); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js index 7c1dd4e475c..79e07f00e7b 100644 --- a/spec/javascripts/ide/components/jobs/item_spec.js +++ b/spec/javascripts/ide/components/jobs/item_spec.js @@ -26,4 +26,14 @@ describe('IDE jobs item', () => { it('renders CI icon', () => { expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null); }); + + it('does not render view logs button if not started', done => { + vm.job.started = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn')).toBe(null); + + done(); + }); + }); }); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index dcf857f7e04..dd87a43f370 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -75,6 +75,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 2, @@ -86,6 +87,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 3, @@ -97,6 +99,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 4, @@ -108,6 +111,7 @@ export const jobs = [ }, stage: 'build', duration: 1, + started: new Date(), }, ]; diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js index f26eaf9c81f..f2f8e780cd1 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -13,9 +13,15 @@ import actions, { receiveJobsSuccess, fetchJobs, toggleStageCollapsed, + setDetailJob, + requestJobTrace, + receiveJobTraceError, + receiveJobTraceSuccess, + fetchJobTrace, } from '~/ide/stores/modules/pipelines/actions'; import state from '~/ide/stores/modules/pipelines/state'; import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { rightSidebarViews } from '~/ide/constants'; import testAction from '../../../../helpers/vuex_action_helper'; import { pipelines, jobs } from '../../../mock_data'; @@ -281,4 +287,133 @@ describe('IDE pipelines actions', () => { ); }); }); + + describe('setDetailJob', () => { + it('commits job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB, payload: 'job' }], + [{ type: 'setRightPane' }], + done, + ); + }); + + it('dispatches setRightPane as pipeline when job is null', done => { + testAction( + setDetailJob, + null, + mockedState, + [{ type: types.SET_DETAIL_JOB }], + [{ type: 'setRightPane', payload: rightSidebarViews.pipelines }], + done, + ); + }); + + it('dispatches setRightPane as job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB }], + [{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }], + done, + ); + }); + }); + + describe('requestJobTrace', () => { + it('commits request', done => { + testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done); + }); + }); + + describe('receiveJobTraceError', () => { + it('commits error', done => { + testAction( + receiveJobTraceError, + null, + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveJobTraceError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveJobTraceSuccess', () => { + it('commits data', done => { + testAction( + receiveJobTraceSuccess, + 'data', + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }], + [], + done, + ); + }); + }); + + describe('fetchJobTrace', () => { + beforeEach(() => { + mockedState.detailJob = { + path: `${gl.TEST_HOST}/project/builds`, + }; + }); + + describe('success', () => { + beforeEach(() => { + spyOn(axios, 'get').and.callThrough(); + mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' }); + }); + + it('dispatches request', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [ + { type: 'requestJobTrace' }, + { type: 'receiveJobTraceSuccess', payload: { html: 'html' } }, + ], + done, + ); + }); + + it('sends get request to correct URL', () => { + fetchJobTrace({ state: mockedState, dispatch() {} }); + + expect(axios.get).toHaveBeenCalledWith(`${gl.TEST_HOST}/project/builds/trace`, { + params: { format: 'json' }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }], + done, + ); + }); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js index 6285c01d483..eb7346bd5fc 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js @@ -147,6 +147,10 @@ describe('IDE pipelines mutations', () => { name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + isLoading: false, + output: '', })), ); }); @@ -171,4 +175,49 @@ describe('IDE pipelines mutations', () => { expect(mockedState.stages[0].isCollapsed).toBe(false); }); }); + + describe(types.SET_DETAIL_JOB, () => { + it('sets detail job', () => { + mutations[types.SET_DETAIL_JOB](mockedState, jobs[0]); + + expect(mockedState.detailJob).toEqual(jobs[0]); + }); + }); + + describe(types.REQUEST_JOB_TRACE, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0] }; + }); + + it('sets loading on detail job', () => { + mutations[types.REQUEST_JOB_TRACE](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(true); + }); + }); + + describe(types.RECEIVE_JOB_TRACE_ERROR, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets loading to false on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); + + describe(types.RECEIVE_JOB_TRACE_SUCCESS, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets output on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' }); + + expect(mockedState.detailJob.output).toBe('html'); + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); }); |