diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js new file mode 100644 index 00000000000..7f5406d6f43 --- /dev/null +++ b/app/assets/javascripts/jobs/store/actions.js @@ -0,0 +1,175 @@ +import Visibility from 'visibilityjs'; +import * as types from './mutation_types'; +import axios from '../../lib/utils/axios_utils'; +import Poll from '../../lib/utils/poll'; +import { setCiStatusFavicon } from '../../lib/utils/common_utils'; +import flash from '../../flash'; +import { __ } from '../../locale'; + +export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); +export const setTraceEndpoint = ({ commit }, endpoint) => + commit(types.SET_TRACE_ENDPOINT, endpoint); +export const setStagesEndpoint = ({ commit }, endpoint) => + commit(types.SET_STAGES_ENDPOINT, endpoint); +export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const requestJob = ({ commit }) => commit(types.REQUEST_JOB); + +export const fetchJob = ({ state, dispatch }) => { + dispatch('requestJob'); + + eTagPoll = new Poll({ + resource: { + getJob(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.jobEndpoint, + method: 'getJob', + successCallback: ({ data }) => dispatch('receiveJobSuccess', data), + errorCallback: () => dispatch('receiveJobError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.jobEndpoint) + .then(({ data }) => dispatch('receiveJobSuccess', data)) + .catch(() => dispatch('receiveJobError')); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data); +export const receiveJobError = ({ commit }) => { + commit(types.RECEIVE_JOB_ERROR); + flash(__('An error occurred while fetching the job.')); +}; + +/** + * Job's Trace + */ +export const scrollTop = ({ commit }) => { + commit(types.SCROLL_TO_TOP); + window.scrollTo({ top: 0 }); +}; + +export const scrollBottom = ({ commit }) => { + commit(types.SCROLL_TO_BOTTOM); + window.scrollTo({ top: document.height }); +}; + +export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); + +let traceTimeout; +export const fetchTrace = ({ dispatch, state }) => { + dispatch('requestTrace'); + + axios + .get(`${state.traceEndpoint}/trace.json`, { + params: { state: state.traceState }, + }) + .then(({ data }) => { + if (!state.fetchingStatusFavicon) { + dispatch('fetchFavicon'); + } + dispatch('receiveTraceSuccess', data); + + if (!data.complete) { + traceTimeout = setTimeout(() => { + dispatch('fetchTrace'); + }, 4000); + } else { + dispatch('stopPollingTrace'); + } + }) + .catch(() => dispatch('receiveTraceError')); +}; +export const stopPollingTrace = ({ commit }) => { + commit(types.STOP_POLLING_TRACE); + clearTimeout(traceTimeout); +}; +export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log); +export const receiveTraceError = ({ commit }) => { + commit(types.RECEIVE_TRACE_ERROR); + clearTimeout(traceTimeout); + flash(__('An error occurred while fetching the job log.')); +}; + +export const fetchFavicon = ({ state, dispatch }) => { + dispatch('requestStatusFavicon'); + setCiStatusFavicon(`${state.pagePath}/status.json`) + .then(() => dispatch('receiveStatusFaviconSuccess')) + .catch(() => dispatch('requestStatusFaviconError')); +}; +export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON); +export const receiveStatusFaviconSuccess = ({ commit }) => + commit(types.RECEIVE_STATUS_FAVICON_SUCCESS); +export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR); + +/** + * Stages dropdown on sidebar + */ +export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES); +export const fetchStages = ({ state, dispatch }) => { + dispatch('requestStages'); + + axios + .get(state.stagesEndpoint) + .then(({ data }) => dispatch('receiveStagesSuccess', data)) + .catch(() => dispatch('receiveStagesError')); +}; +export const receiveStagesSuccess = ({ commit }, data) => + commit(types.RECEIVE_STAGES_SUCCESS, data); +export const receiveStagesError = ({ commit }) => { + commit(types.RECEIVE_STAGES_ERROR); + flash(__('An error occurred while fetching stages.')); +}; + +/** + * Jobs list on sidebar - depend on stages dropdown + */ +export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); +export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage); + +// On stage click, set selected stage + fetch job +export const fetchJobsForStage = ({ state, dispatch }, stage) => { + dispatch('setSelectedStage', stage); + dispatch('requestJobsForStage'); + + axios + .get(state.stageJobsEndpoint) + .then(({ data }) => dispatch('receiveJobsForStageSuccess', data)) + .catch(() => dispatch('receiveJobsForStageError')); +}; +export const receiveJobsForStageSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data); +export const receiveJobsForStageError = ({ commit }) => { + commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR); + flash(__('An error occurred while fetching the jobs.')); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js new file mode 100644 index 00000000000..d8f6f56ce61 --- /dev/null +++ b/app/assets/javascripts/jobs/store/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default () => new Vuex.Store({ + actions, + mutations, + state: state(), +}); diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js new file mode 100644 index 00000000000..e66e1d4f116 --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -0,0 +1,29 @@ +export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT'; +export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT'; +export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT'; +export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT'; + +export const SCROLL_TO_TOP = 'SCROLL_TO_TOP'; +export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM'; + +export const REQUEST_JOB = 'REQUEST_JOB'; +export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; +export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR'; + +export const REQUEST_TRACE = 'REQUEST_TRACE'; +export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; +export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; +export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; + +export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON'; +export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS'; +export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR'; + +export const REQUEST_STAGES = 'REQUEST_STAGES'; +export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; +export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; + +export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; +export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; +export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS'; +export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js new file mode 100644 index 00000000000..2a451ef0cd1 --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -0,0 +1,94 @@ +/* eslint-disable no-param-reassign */ + +import * as types from './mutation_types'; + +export default { + [types.REQUEST_STATUS_FAVICON](state) { + state.fetchingStatusFavicon = true; + }, + [types.RECEIVE_STATUS_FAVICON_SUCCESS](state) { + state.fetchingStatusFavicon = false; + }, + [types.RECEIVE_STATUS_FAVICON_ERROR](state) { + state.fetchingStatusFavicon = false; + }, + + [types.RECEIVE_TRACE_SUCCESS](state, log) { + if (log.state) { + state.traceState = log.state; + } + + if (log.append) { + state.trace += log.html; + state.traceSize += log.size; + } else { + state.trace = log.html; + state.traceSize = log.size; + } + + if (state.traceSize < log.total) { + state.isTraceSizeVisible = true; + } else { + state.isTraceSizeVisible = false; + } + + state.isTraceComplete = log.complete; + state.hasTraceError = false; + }, + [types.STOP_POLLING_TRACE](state) { + state.isTraceComplete = true; + }, + // todo_fl: check this. + [types.RECEIVE_TRACE_ERROR](state) { + state.isLoadingTrace = false; + state.isTraceComplete = true; + state.hasTraceError = true; + }, + + [types.REQUEST_JOB](state) { + state.isLoading = true; + }, + [types.RECEIVE_JOB_SUCCESS](state, job) { + state.isLoading = false; + state.hasError = false; + state.job = job; + }, + [types.RECEIVE_JOB_ERROR](state) { + state.isLoading = false; + state.hasError = true; + state.job = {}; + }, + + [types.SCROLL_TO_TOP](state) { + state.isTraceScrolledToBottom = false; + state.hasBeenScrolled = true; + }, + [types.SCROLL_TO_BOTTOM](state) { + state.isTraceScrolledToBottom = true; + state.hasBeenScrolled = true; + }, + + [types.REQUEST_STAGES](state) { + state.isLoadingStages = true; + }, + [types.RECEIVE_STAGES_SUCCESS](state, stages) { + state.isLoadingStages = false; + state.stages = stages; + }, + [types.RECEIVE_STAGES_ERROR](state) { + state.isLoadingStages = false; + state.stages = []; + }, + + [types.REQUEST_JOBS_FOR_STAGE](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + state.jobs = jobs; + }, + [types.RECEIVE_JOBS_FOR_STAGE_ERROR](state) { + state.isLoadingJobs = false; + state.jobs = []; + }, +}; diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js new file mode 100644 index 00000000000..509cb69a5d3 --- /dev/null +++ b/app/assets/javascripts/jobs/store/state.js @@ -0,0 +1,40 @@ +export default () => ({ + jobEndpoint: null, + traceEndpoint: null, + + // dropdown options + stagesEndpoint: null, + // list of jobs on sidebard + stageJobsEndpoint: null, + + // job log + isLoading: false, + hasError: false, + job: {}, + + // trace + isLoadingTrace: false, + hasTraceError: false, + + trace: '', + + isTraceScrolledToBottom: false, + hasBeenScrolled: false, + + isTraceComplete: false, + traceSize: 0, // todo_fl: needs to be converted into human readable format in components + isTraceSizeVisible: false, + + fetchingStatusFavicon: false, + // used as a query parameter + traceState: null, + // used to check if we need to redirect the user - todo_fl: check if actually needed + traceStatus: null, + + // sidebar dropdown + isLoadingStages: false, + isLoadingJobs: false, + selectedStage: null, + stages: [], + jobs: [], +}); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2f3dd6f6cbc..3e208764b3e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl => } return resetFavicon(); }) - .catch(resetFavicon); + .catch((error) => { + resetFavicon(); + throw error; + }); export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 936b85146d4..2eb69695406 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -505,6 +505,18 @@ msgstr "" msgid "An error occurred while fetching sidebar data" msgstr "" +msgid "An error occurred while fetching stages." +msgstr "" + +msgid "An error occurred while fetching the job log." +msgstr "" + +msgid "An error occurred while fetching the job." +msgstr "" + +msgid "An error occurred while fetching the jobs." +msgstr "" + msgid "An error occurred while fetching the pipeline." msgstr "" diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js new file mode 100644 index 00000000000..5042718dfa0 --- /dev/null +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -0,0 +1,625 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { + setJobEndpoint, + setTraceEndpoint, + setStagesEndpoint, + setJobsEndpoint, + clearEtagPoll, + stopPolling, + requestJob, + fetchJob, + receiveJobSuccess, + receiveJobError, + scrollTop, + scrollBottom, + requestTrace, + fetchTrace, + stopPollingTrace, + receiveTraceSuccess, + receiveTraceError, + fetchFavicon, + requestStatusFavicon, + receiveStatusFaviconSuccess, + requestStatusFaviconError, + requestStages, + fetchStages, + receiveStagesSuccess, + receiveStagesError, + requestJobsForStage, + setSelectedStage, + fetchJobsForStage, + receiveJobsForStageSuccess, + receiveJobsForStageError, +} from '~/jobs/store/actions'; +import state from '~/jobs/store/state'; +import * as types from '~/jobs/store/mutation_types'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('Job State actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setJobEndpoint', () => { + it('should commit SET_JOB_ENDPOINT mutation', done => { + testAction( + setJobEndpoint, + 'job/872324.json', + mockedState, + [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }], + [], + done, + ); + }); + }); + + describe('setTraceEndpoint', () => { + it('should commit SET_TRACE_ENDPOINT mutation', done => { + testAction( + setTraceEndpoint, + 'job/872324/trace.json', + mockedState, + [{ type: types.SET_TRACE_ENDPOINT, payload: 'job/872324/trace.json' }], + [], + done, + ); + }); + }); + + describe('setStagesEndpoint', () => { + it('should commit SET_STAGES_ENDPOINT mutation', done => { + testAction( + setStagesEndpoint, + 'job/872324/stages.json', + mockedState, + [{ type: types.SET_STAGES_ENDPOINT, payload: 'job/872324/stages.json' }], + [], + done, + ); + }); + }); + + describe('setJobsEndpoint', () => { + it('should commit SET_JOBS_ENDPOINT mutation', done => { + testAction( + setJobsEndpoint, + 'job/872324/stages/build.json', + mockedState, + [{ type: types.SET_JOBS_ENDPOINT, payload: 'job/872324/stages/build.json' }], + [], + done, + ); + }); + }); + + describe('requestJob', () => { + it('should commit REQUEST_JOB mutation', done => { + testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done); + }); + }); + + describe('fetchJob', () => { + let mock; + + beforeEach(() => { + mockedState.jobEndpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestJob and receiveJobSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' }); + + testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + payload: { id: 121212, name: 'karma' }, + type: 'receiveJobSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches requestJob and receiveJobError ', done => { + testAction( + fetchJob, + null, + mockedState, + [], + [ + { + type: 'requestJob', + }, + { + type: 'receiveJobError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveJobSuccess', () => { + it('should commit RECEIVE_JOB_SUCCESS mutation', done => { + testAction( + receiveJobSuccess, + { id: 121232132 }, + mockedState, + [{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }], + [], + done, + ); + }); + }); + + describe('receiveJobError', () => { + it('should commit RECEIVE_JOB_ERROR mutation', done => { + testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done); + }); + }); + + describe('scrollTop', () => { + it('should commit SCROLL_TO_TOP mutation', done => { + testAction(scrollTop, null, mockedState, [{ type: types.SCROLL_TO_TOP }], [], done); + }); + }); + + describe('scrollBottom', () => { + it('should commit SCROLL_TO_BOTTOM mutation', done => { + testAction(scrollBottom, null, mockedState, [{ type: types.SCROLL_TO_BOTTOM }], [], done); + }); + }); + + describe('requestTrace', () => { + it('should commit REQUEST_TRACE mutation', done => { + testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done); + }); + }); + + describe('fetchTrace', () => { + let mock; + + beforeEach(() => { + mockedState.traceEndpoint = `${TEST_HOST}/endpoint`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestTrace, fetchFavicon, receiveTraceSuccess and stopPollingTrace when job is complete', done => { + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', + complete: true, + }); + + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { + type: 'requestTrace', + }, + { + type: 'fetchFavicon', + }, + { + payload: { + html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', complete: true, + }, + type: 'receiveTraceSuccess', + }, + { + type: 'stopPollingTrace', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500); + }); + + it('dispatches requestTrace and receiveTraceError ', done => { + testAction( + fetchTrace, + null, + mockedState, + [], + [ + { + type: 'requestTrace', + }, + { + type: 'receiveTraceError', + }, + ], + done, + ); + }); + }); + }); + + describe('stopPollingTrace', () => { + it('should commit STOP_POLLING_TRACE mutation ', done => { + testAction( + stopPollingTrace, + null, + mockedState, + [{ type: types.STOP_POLLING_TRACE }], + [], + done, + ); + }); + }); + + describe('receiveTraceSuccess', () => { + it('should commit RECEIVE_TRACE_SUCCESS mutation ', done => { + testAction( + receiveTraceSuccess, + 'hello world', + mockedState, + [{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }], + [], + done, + ); + }); + }); + + describe('receiveTraceError', () => { + it('should commit RECEIVE_TRACE_ERROR mutation ', done => { + testAction( + receiveTraceError, + null, + mockedState, + [{ type: types.RECEIVE_TRACE_ERROR }], + [], + done, + ); + }); + }); + + describe('fetchFavicon', () => { + let mock; + + beforeEach(() => { + mockedState.pagePath = `${TEST_HOST}/endpoint`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestStatusFavicon and receiveStatusFaviconSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(200); + + testAction( + fetchFavicon, + null, + mockedState, + [], + [ + { + type: 'requestStatusFavicon', + }, + { + type: 'receiveStatusFaviconSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint/status.json`).replyOnce(500); + }); + + it('dispatches requestStatusFavicon and requestStatusFaviconError ', done => { + testAction( + fetchFavicon, + null, + mockedState, + [], + [ + { + type: 'requestStatusFavicon', + }, + { + type: 'requestStatusFaviconError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestStatusFavicon', () => { + it('should commit REQUEST_STATUS_FAVICON mutation ', done => { + testAction( + requestStatusFavicon, + null, + mockedState, + [{ type: types.REQUEST_STATUS_FAVICON }], + [], + done, + ); + }); + }); + + describe('receiveStatusFaviconSuccess', () => { + it('should commit RECEIVE_STATUS_FAVICON_SUCCESS mutation ', done => { + testAction( + receiveStatusFaviconSuccess, + null, + mockedState, + [{ type: types.RECEIVE_STATUS_FAVICON_SUCCESS }], + [], + done, + ); + }); + }); + + describe('requestStatusFaviconError', () => { + it('should commit RECEIVE_STATUS_FAVICON_ERROR mutation ', done => { + testAction( + requestStatusFaviconError, + null, + mockedState, + [{ type: types.RECEIVE_STATUS_FAVICON_ERROR }], + [], + done, + ); + }); + }); + + describe('requestStages', () => { + it('should commit REQUEST_STAGES mutation ', done => { + testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done); + }); + }); + + describe('fetchStages', () => { + let mock; + + beforeEach(() => { + mockedState.stagesEndpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestStages and receiveStagesSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]); + + testAction( + fetchStages, + null, + mockedState, + [], + [ + { + type: 'requestStages', + }, + { + payload: [{ id: 121212, name: 'build' }], + type: 'receiveStagesSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches requestStages and receiveStagesError ', done => { + testAction( + fetchStages, + null, + mockedState, + [], + [ + { + type: 'requestStages', + }, + { + type: 'receiveStagesError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveStagesSuccess', () => { + it('should commit RECEIVE_STAGES_SUCCESS mutation ', done => { + testAction( + receiveStagesSuccess, + {}, + mockedState, + [{ type: types.RECEIVE_STAGES_SUCCESS, payload: {} }], + [], + done, + ); + }); + }); + + describe('receiveStagesError', () => { + it('should commit RECEIVE_STAGES_ERROR mutation ', done => { + testAction( + receiveStagesError, + null, + mockedState, + [{ type: types.RECEIVE_STAGES_ERROR }], + [], + done, + ); + }); + }); + + describe('requestJobsForStage', () => { + it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => { + testAction( + requestJobsForStage, + null, + mockedState, + [{ type: types.REQUEST_JOBS_FOR_STAGE }], + [], + done, + ); + }); + }); + + describe('setSelectedStage', () => { + it('should commit SET_SELECTED_STAGE mutation ', done => { + testAction( + setSelectedStage, + { name: 'build' }, + mockedState, + [{ type: types.SET_SELECTED_STAGE, payload: { name: 'build' } }], + [], + done, + ); + }); + }); + + describe('fetchJobsForStage', () => { + let mock; + + beforeEach(() => { + mockedState.stageJobsEndpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]); + + testAction( + fetchJobsForStage, + null, + mockedState, + [], + [ + { + type: 'setSelectedStage', + payload: null, + }, + { + type: 'requestJobsForStage', + }, + { + payload: [{ id: 121212, name: 'build' }], + type: 'receiveJobsForStageSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageError', done => { + testAction( + fetchJobsForStage, + null, + mockedState, + [], + [ + { + payload: null, + type: 'setSelectedStage', + }, + { + type: 'requestJobsForStage', + }, + { + type: 'receiveJobsForStageError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveJobsForStageSuccess', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', done => { + testAction( + receiveJobsForStageSuccess, + [{ id: 121212, name: 'karma' }], + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }], + [], + done, + ); + }); + }); + + describe('receiveJobsForStageError', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', done => { + testAction( + receiveJobsForStageError, + null, + mockedState, + [{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js new file mode 100644 index 00000000000..6900b2e5602 --- /dev/null +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -0,0 +1,228 @@ +import state from '~/jobs/store/state'; +import mutations from '~/jobs/store/mutations'; +import * as types from '~/jobs/store/mutation_types'; + +describe('Jobs Store Mutations', () => { + let stateCopy; + + const html = + 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png
I'; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('REQUEST_STATUS_FAVICON', () => { + it('should set fetchingStatusFavicon to true', () => { + mutations[types.REQUEST_STATUS_FAVICON](stateCopy); + expect(stateCopy.fetchingStatusFavicon).toEqual(true); + }); + }); + + describe('RECEIVE_STATUS_FAVICON_SUCCESS', () => { + it('should set fetchingStatusFavicon to false', () => { + mutations[types.RECEIVE_STATUS_FAVICON_SUCCESS](stateCopy); + expect(stateCopy.fetchingStatusFavicon).toEqual(false); + }); + }); + + describe('RECEIVE_STATUS_FAVICON_ERROR', () => { + it('should set fetchingStatusFavicon to false', () => { + mutations[types.RECEIVE_STATUS_FAVICON_ERROR](stateCopy); + expect(stateCopy.fetchingStatusFavicon).toEqual(false); + }); + }); + + describe('RECEIVE_TRACE_SUCCESS', () => { + describe('when trace has state', () => { + it('sets traceState', () => { + const stateLog = + 'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0='; + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { + state: stateLog, + }); + expect(stateCopy.traceState).toEqual(stateLog); + }); + }); + + describe('when traceSize is smaller than the total size', () => { + it('sets isTraceSizeVisible to true', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { total: 51184600, size: 1231 }); + + expect(stateCopy.isTraceSizeVisible).toEqual(true); + }); + }); + + describe('when traceSize is bigger than the total size', () => { + it('sets isTraceSizeVisible to false', () => { + const copy = Object.assign({}, stateCopy, { traceSize: 5118460, size: 2321312 }); + + mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 }); + + expect(copy.isTraceSizeVisible).toEqual(false); + }); + }); + + it('sets trace, trace size and isTraceComplete', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { + append: true, + html, + size: 511846, + complete: true, + }); + expect(stateCopy.trace).toEqual(html); + expect(stateCopy.traceSize).toEqual(511846); + expect(stateCopy.isTraceComplete).toEqual(true); + }); + }); + + describe('STOP_POLLING_TRACE', () => { + it('sets isTraceComplete to true', () => { + mutations[types.STOP_POLLING_TRACE](stateCopy); + expect(stateCopy.isTraceComplete).toEqual(true); + }); + }); + + describe('RECEIVE_TRACE_ERROR', () => { + it('resets trace state and sets error to true', () => { + mutations[types.RECEIVE_TRACE_ERROR](stateCopy); + expect(stateCopy.isLoadingTrace).toEqual(false); + expect(stateCopy.isTraceComplete).toEqual(true); + expect(stateCopy.hasTraceError).toEqual(true); + }); + }); + + describe('REQUEST_JOB', () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_JOB](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_JOB_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); + }); + + it('sets is loading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('sets hasError to false', () => { + expect(stateCopy.hasError).toEqual(false); + }); + + it('sets job data', () => { + expect(stateCopy.job).toEqual({ id: 1312321 }); + }); + }); + + describe('RECEIVE_JOB_ERROR', () => { + it('resets job data', () => { + mutations[types.RECEIVE_JOB_ERROR](stateCopy); + + expect(stateCopy.isLoading).toEqual(false); + expect(stateCopy.hasError).toEqual(true); + expect(stateCopy.job).toEqual({}); + }); + }); + + describe('SCROLL_TO_TOP', () => { + beforeEach(() => { + mutations[types.SCROLL_TO_TOP](stateCopy); + }); + + it('sets isTraceScrolledToBottom to false', () => { + expect(stateCopy.isTraceScrolledToBottom).toEqual(false); + }); + + it('sets hasBeenScrolled to true', () => { + expect(stateCopy.hasBeenScrolled).toEqual(true); + }); + }); + + describe('SCROLL_TO_BOTTOM', () => { + beforeEach(() => { + mutations[types.SCROLL_TO_BOTTOM](stateCopy); + }); + + it('sets isTraceScrolledToBottom to true', () => { + expect(stateCopy.isTraceScrolledToBottom).toEqual(true); + }); + + it('sets hasBeenScrolled to true', () => { + expect(stateCopy.hasBeenScrolled).toEqual(true); + }); + }); + + describe('REQUEST_STAGES', () => { + it('sets isLoadingStages to true', () => { + mutations[types.REQUEST_STAGES](stateCopy); + expect(stateCopy.isLoadingStages).toEqual(true); + }); + }); + + describe('RECEIVE_STAGES_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_STAGES_SUCCESS](stateCopy, [{ name: 'build' }]); + }); + + it('sets isLoadingStages to false', () => { + expect(stateCopy.isLoadingStages).toEqual(false); + }); + + it('sets stages', () => { + expect(stateCopy.stages).toEqual([{ name: 'build' }]); + }); + }); + + describe('RECEIVE_STAGES_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_STAGES_ERROR](stateCopy); + }); + + it('sets isLoadingStages to false', () => { + expect(stateCopy.isLoadingStages).toEqual(false); + }); + + it('resets stages', () => { + expect(stateCopy.stages).toEqual([]); + }); + }); + + describe('REQUEST_JOBS_FOR_STAGE', () => { + it('sets isLoadingStages to true', () => { + mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy); + expect(stateCopy.isLoadingJobs).toEqual(true); + }); + }); + + describe('RECEIVE_JOBS_FOR_STAGE_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](stateCopy, [{ name: 'karma' }]); + }); + + it('sets isLoadingJobs to false', () => { + expect(stateCopy.isLoadingJobs).toEqual(false); + }); + + it('sets jobs', () => { + expect(stateCopy.jobs).toEqual([{ name: 'karma' }]); + }); + }); + + describe('RECEIVE_JOBS_FOR_STAGE_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_JOBS_FOR_STAGE_ERROR](stateCopy); + }); + + it('sets isLoadingJobs to false', () => { + expect(stateCopy.isLoadingJobs).toEqual(false); + }); + + it('resets jobs', () => { + expect(stateCopy.jobs).toEqual([]); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 71b26a315af..babad296f09 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -403,6 +403,7 @@ describe('common_utils', () => { afterEach(() => { document.body.removeChild(document.getElementById('favicon')); }); + it('should set page favicon to provided favicon', () => { const faviconPath = '//custom_favicon'; commonUtils.setFavicon(faviconPath); @@ -479,17 +480,14 @@ describe('common_utils', () => { }); it('should reset favicon in case of error', (done) => { - mock.onGet(BUILD_URL).networkError(); + mock.onGet(BUILD_URL).replyOnce(500); commonUtils.setCiStatusFavicon(BUILD_URL) - .then(() => { + .catch(() => { const favicon = document.getElementById('favicon'); expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); done(); - }) - // Error is already caught in catch() block of setCiStatusFavicon, - // It won't throw another error for us to catch - .catch(done.fail); + }); }); it('should set page favicon to CI status favicon based on provided status', (done) => {