Adds Vuex store to handle the data for tests reports in MR widget
This commit is contained in:
parent
4636bebb68
commit
2d56c8fdd7
8 changed files with 375 additions and 0 deletions
67
app/assets/javascripts/reports/store/actions.js
Normal file
67
app/assets/javascripts/reports/store/actions.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import axios from '../../lib/utils/axios_utils';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
|
||||
|
||||
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
|
||||
|
||||
let eTagPoll;
|
||||
|
||||
export const clearEtagPoll = () => {
|
||||
eTagPoll = null;
|
||||
};
|
||||
|
||||
export const stopPolling = () => {
|
||||
if (eTagPoll) eTagPoll.stop();
|
||||
};
|
||||
|
||||
export const restartPolling = () => {
|
||||
if (eTagPoll) eTagPoll.restart();
|
||||
};
|
||||
|
||||
/**
|
||||
* We need to poll the reports endpoint while they are being parsed in the Backend.
|
||||
* This can take up to one minute.
|
||||
*
|
||||
* Poll.js will handle etag response.
|
||||
* While http status code is 204, it means it's parsing, and we'll keep polling
|
||||
* When http status code is 200, it means parsing is done, we can show the results & stop polling
|
||||
* When http status code is 500, it means parsing went wrong and we stop polling
|
||||
*/
|
||||
export const fetchReports = ({ state, dispatch }) => {
|
||||
dispatch('requestReports');
|
||||
|
||||
eTagPoll = new Poll({
|
||||
resource: {
|
||||
getReports(endpoint) {
|
||||
return axios.get(endpoint);
|
||||
},
|
||||
},
|
||||
data: state.endpoint,
|
||||
method: 'getReports',
|
||||
successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
|
||||
errorCallback: () => dispatch('receiveReportsError'),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
dispatch('restartPolling');
|
||||
} else {
|
||||
dispatch('stopPolling');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const receiveReportsSuccess = ({ commit }, response) =>
|
||||
commit(types.RECEIVE_REPORTS_SUCCESS, response);
|
||||
|
||||
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
13
app/assets/javascripts/reports/store/index.js
Normal file
13
app/assets/javascripts/reports/store/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default () => new Vuex.Store({
|
||||
actions,
|
||||
mutations,
|
||||
state: state(),
|
||||
});
|
5
app/assets/javascripts/reports/store/mutation_types.js
Normal file
5
app/assets/javascripts/reports/store/mutation_types.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const SET_ENDPOINT = 'SET_ENDPOINT';
|
||||
|
||||
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
|
||||
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
|
||||
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
|
26
app/assets/javascripts/reports/store/mutations.js
Normal file
26
app/assets/javascripts/reports/store/mutations.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_ENDPOINT](state, endpoint) {
|
||||
state.endpoint = endpoint;
|
||||
},
|
||||
[types.REQUEST_REPORTS](state) {
|
||||
state.isLoading = true;
|
||||
},
|
||||
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
|
||||
|
||||
state.isLoading = false;
|
||||
|
||||
state.summary.total = response.summary.total;
|
||||
state.summary.resolved = response.summary.resolved;
|
||||
state.summary.failed = response.summary.failed;
|
||||
|
||||
state.reports = response.suites;
|
||||
|
||||
},
|
||||
[types.RECEIVE_REPORTS_ERROR](state) {
|
||||
state.isLoading = false;
|
||||
state.hasError = true;
|
||||
},
|
||||
};
|
28
app/assets/javascripts/reports/store/state.js
Normal file
28
app/assets/javascripts/reports/store/state.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
export default () => ({
|
||||
endpoint: null,
|
||||
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
|
||||
summary: {
|
||||
total: 0,
|
||||
resolved: 0,
|
||||
failed: 0,
|
||||
},
|
||||
|
||||
/**
|
||||
* Each report will have the following format:
|
||||
* {
|
||||
* name: {String},
|
||||
* summary: {
|
||||
* total: {Number},
|
||||
* resolved: {Number},
|
||||
* failed: {Number},
|
||||
* },
|
||||
* new_failures: {Array.<Object>},
|
||||
* resolved_failures: {Array.<Object>},
|
||||
* existing_failures: {Array.<Object>},
|
||||
* }
|
||||
*/
|
||||
reports: [],
|
||||
});
|
5
changelogs/unreleased/45318-vuex-store.yml
Normal file
5
changelogs/unreleased/45318-vuex-store.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds Vuex store for reports section in MR widget
|
||||
merge_request: 20709
|
||||
author:
|
||||
type: added
|
130
spec/javascripts/reports/store/actions_spec.js
Normal file
130
spec/javascripts/reports/store/actions_spec.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import {
|
||||
setEndpoint,
|
||||
requestReports,
|
||||
fetchReports,
|
||||
stopPolling,
|
||||
clearEtagPoll,
|
||||
receiveReportsSuccess,
|
||||
receiveReportsError,
|
||||
} from '~/reports/store/actions';
|
||||
import state from '~/reports/store/state';
|
||||
import * as types from '~/reports/store/mutation_types';
|
||||
import testAction from 'spec/helpers/vuex_action_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
|
||||
describe('Reports Store Actions', () => {
|
||||
let mockedState;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedState = state();
|
||||
});
|
||||
|
||||
describe('setEndpoint', () => {
|
||||
it('should commit SET_ENDPOINT mutation', done => {
|
||||
testAction(
|
||||
setEndpoint,
|
||||
'endpoint.json',
|
||||
mockedState,
|
||||
[{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestReports', () => {
|
||||
it('should commit REQUEST_REPORTS mutation', done => {
|
||||
testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchReports', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
stopPolling();
|
||||
clearEtagPoll();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
it('dispatches requestReports and receiveReportsSuccess ', done => {
|
||||
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
|
||||
|
||||
testAction(
|
||||
fetchReports,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'requestReports',
|
||||
},
|
||||
{
|
||||
payload: { summary: {}, suites: [{ name: 'rspec' }] },
|
||||
type: 'receiveReportsSuccess',
|
||||
},
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
|
||||
});
|
||||
|
||||
it('dispatches requestReports and receiveReportsError ', done => {
|
||||
testAction(
|
||||
fetchReports,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'requestReports',
|
||||
},
|
||||
{
|
||||
type: 'receiveReportsError',
|
||||
},
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveReportsSuccess', () => {
|
||||
it('should commit RECEIVE_REPORTS_SUCCESS mutation', done => {
|
||||
testAction(
|
||||
receiveReportsSuccess,
|
||||
{ summary: {} },
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveReportsError', () => {
|
||||
it('should commit RECEIVE_REPORTS_ERROR mutation', done => {
|
||||
testAction(
|
||||
receiveReportsError,
|
||||
null,
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_REPORTS_ERROR }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
101
spec/javascripts/reports/store/mutations_spec.js
Normal file
101
spec/javascripts/reports/store/mutations_spec.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import state from '~/reports/store/state';
|
||||
import mutations from '~/reports/store/mutations';
|
||||
import * as types from '~/reports/store/mutation_types';
|
||||
|
||||
describe('Reports Store Mutations', () => {
|
||||
let stateCopy;
|
||||
|
||||
beforeEach(() => {
|
||||
stateCopy = state();
|
||||
});
|
||||
|
||||
describe('SET_ENDPOINT', () => {
|
||||
it('should set endpoint', () => {
|
||||
mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json');
|
||||
expect(stateCopy.endpoint).toEqual('endpoint.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_REPORTS', () => {
|
||||
it('should set isLoading to true', () => {
|
||||
mutations[types.REQUEST_REPORTS](stateCopy);
|
||||
expect(stateCopy.isLoading).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_REPORTS_SUCCESS', () => {
|
||||
const mockedResponse = {
|
||||
summary: {
|
||||
total: 14,
|
||||
resolved: 0,
|
||||
failed: 7,
|
||||
},
|
||||
suites: [
|
||||
{
|
||||
name: 'build:linux',
|
||||
summary: {
|
||||
total: 2,
|
||||
resolved: 0,
|
||||
failed: 1,
|
||||
},
|
||||
new_failures: [
|
||||
{
|
||||
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
|
||||
execution_time: 0.0092435,
|
||||
system_output:
|
||||
'Failure/Error: is_expected.to eq(\'gitlab\')',
|
||||
},
|
||||
],
|
||||
resolved_failures: [
|
||||
{
|
||||
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
|
||||
execution_time: 0.009235,
|
||||
system_output:
|
||||
'Failure/Error: is_expected.to eq(\'gitlab\')',
|
||||
},
|
||||
],
|
||||
existing_failures: [
|
||||
{
|
||||
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
|
||||
execution_time: 1232.08,
|
||||
system_output:
|
||||
'Failure/Error: is_expected.to eq(\'gitlab\')',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse);
|
||||
});
|
||||
|
||||
it('should reset isLoading', () => {
|
||||
expect(stateCopy.isLoading).toEqual(false);
|
||||
});
|
||||
|
||||
it('should set summary counts', () => {
|
||||
expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
|
||||
expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
|
||||
expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
|
||||
});
|
||||
|
||||
it('should set reports', () => {
|
||||
expect(stateCopy.reports).toEqual(mockedResponse.suites);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_REPORTS_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
|
||||
});
|
||||
it('should reset isLoading', () => {
|
||||
expect(stateCopy.isLoading).toEqual(false);
|
||||
});
|
||||
|
||||
it('should set hasError to true', () => {
|
||||
expect(stateCopy.hasError).toEqual(true);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue