summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author🌴🌴 Filipa Lacerda - OOO back on September 17th 🌴🌴 <filipa@gitlab.com>2018-09-03 07:49:52 +0000
committerPhil Hughes <me@iamphill.com>2018-09-03 07:49:52 +0000
commit55582b4359a4c46b5cbb4fa60d93ca92cc947063 (patch)
tree1dc83ec5bba15d99ea5909eb1571c5ef99ef9fae
parente550b1abaaa9b0053d3430377938f1e801c1fbb1 (diff)
downloadgitlab-ce-55582b4359a4c46b5cbb4fa60d93ca92cc947063.tar.gz
Adds Vuex store for the job log page
-rw-r--r--app/assets/javascripts/jobs/store/actions.js175
-rw-r--r--app/assets/javascripts/jobs/store/index.js13
-rw-r--r--app/assets/javascripts/jobs/store/mutation_types.js29
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js94
-rw-r--r--app/assets/javascripts/jobs/store/state.js40
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js625
-rw-r--r--spec/javascripts/jobs/store/mutations_spec.js228
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js10
10 files changed, 1224 insertions, 7 deletions
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<br>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) => {