Commit 160f182b authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Enrique Alcántara

Fetch project VSA medians from group VSA endpoint

parent 856a51d3
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const GROUP_VSA_PATH_BASE =
'/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
......@@ -13,6 +15,12 @@ const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
};
const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) =>
buildApiUrl(GROUP_VSA_PATH_BASE)
.replace(':id', groupId)
.replace(':value_stream_id', valueStreamId)
.replace(':stage_id', stageId);
export const getProjectValueStreams = (projectPath) => {
const url = buildProjectValueStreamPath(projectPath);
return axios.get(url);
......@@ -30,3 +38,14 @@ export const getProjectValueStreamStageData = ({ requestPath, stageId, params })
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
/**
* Shared group VSA paths
* We share some endpoints across and group and project level VSA
* When used for project level VSA, requests should include the `project_id` in the params object
*/
export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => {
const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId });
return axios.get(`${stageBase}/median`, { params });
};
......@@ -8,11 +8,24 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
const {
noAccessSvgPath,
noDataSvgPath,
requestPath,
fullPath,
projectId,
groupPath,
} = el.dataset;
store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10),
groupPath,
requestPath,
fullPath,
features: {
cycleAnalyticsForGroups:
(groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
},
});
// eslint-disable-next-line no-new
......
......@@ -3,6 +3,7 @@ import {
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
getValueStreamStageMedian,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
......@@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
};
export const fetchValueStreams = ({ commit, dispatch, state }) => {
const { fullPath } = state;
const {
fullPath,
features: { cycleAnalyticsForGroups },
} = state;
commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage'];
if (cycleAnalyticsForGroups) {
stageRequests.push('fetchStageMedians');
}
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => dispatch('setSelectedStage'))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
export const fetchCycleAnalyticsData = ({
state: { requestPath },
getters: { legacyFilterParams },
commit,
}) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
return getProjectValueStreamMetrics(requestPath, legacyFilterParams)
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
......@@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
});
};
export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
export const fetchStageData = ({
state: { requestPath, selectedStage },
getters: { legacyFilterParams },
commit,
}) => {
commit(types.REQUEST_STAGE_DATA);
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
params: { 'cycle_analytics[start_date]': startDate },
params: legacyFilterParams,
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
......@@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};
const getStageMedians = ({ stageId, vsaParams, filterParams = {} }) => {
return getValueStreamStageMedian({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
id: stageId,
value: data?.value || null,
}));
};
export const fetchStageMedians = ({
state: { stages },
getters: { requestParams: vsaParams, filterParams },
commit,
}) => {
commit(types.REQUEST_STAGE_MEDIANS);
return Promise.all(
stages.map(({ id: stageId }) =>
getStageMedians({
vsaParams,
stageId,
filterParams,
}),
),
)
.then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
createFlash({
message: __('There was an error fetching median data for stages'),
});
});
};
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
......
import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
......@@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
selectedStage,
});
};
export const requestParams = (state) => {
const {
selectedStage: { id: stageId = null },
groupPath: groupId,
selectedValueStream: { id: valueStreamId },
} = state;
return { valueStreamId, groupId, stageId };
};
const dateRangeParams = ({ createdAfter, createdBefore }) => ({
created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
});
export const legacyFilterParams = ({ startDate }) => {
return {
'cycle_analytics[start_date]': startDate,
};
};
export const filterParams = ({ id, ...rest }) => {
return {
project_ids: [id],
...dateRangeParams(rest),
};
};
......@@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_
export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { decorateData, decorateEvents, formatMedianValues } from '../utils';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
import {
decorateData,
decorateEvents,
formatMedianValues,
calculateFormattedDayInPast,
} from '../utils';
import * as types from './mutation_types';
export default {
[types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
[types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) {
state.requestPath = requestPath;
state.fullPath = fullPath;
state.groupPath = groupPath;
state.id = projectId;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
state.createdBefore = now;
state.createdAfter = past;
state.features = features;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
......@@ -18,6 +30,9 @@ export default {
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
const { now, past } = calculateFormattedDayInPast(startDate);
state.createdBefore = now;
state.createdAfter = past;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
......@@ -46,17 +61,25 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.hasError = false;
if (!state.features.cycleAnalyticsForGroups) {
state.medians = {};
}
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
const { summary, medians } = decorateData(data);
if (!state.features.cycleAnalyticsForGroups) {
state.medians = formatMedianValues(medians);
}
state.permissions = data.permissions;
state.summary = summary;
state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.hasError = true;
if (!state.features.cycleAnalyticsForGroups) {
state.medians = {};
}
},
[types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true;
......@@ -78,4 +101,13 @@ export default {
state.hasError = true;
state.selectedStageError = error;
},
[types.REQUEST_STAGE_MEDIANS](state) {
state.medians = {};
},
[types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians) {
state.medians = formatMedianValues(medians);
},
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {};
},
};
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
features: {},
id: null,
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null,
createdBefore: null,
stages: [],
summary: [],
analytics: [],
......@@ -19,4 +23,5 @@ export default () => ({
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
parentPath: null,
});
import dateFormat from 'dateformat';
import { unescape } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
......@@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
/**
* Takes an integer specifying the number of days to subtract
* from the date specified will return the 2 dates, formatted as ISO dates
*
* @param {Number} daysInPast - Number of days in the past to subtract
* @param {Date} [today=new Date] - Date to subtract days from, defaults to today
* @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
*/
export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
return {
now: toIsoFormat(today),
past: toIsoFormat(getDateInPast(today, daysInPast)),
};
};
......@@ -13,6 +13,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
feature_category :planning_analytics
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
end
def show
@cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params))
......
- page_title _("Value Stream Analytics")
- add_page_specific_style 'page_bundles/cycle_analytics'
- svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
- initial_data = { request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
#js-cycle-analytics{ data: initial_data }
import Api from 'ee/api';
import { getValueStreamStageMedian } from '~/api/analytics_api';
import createFlash from '~/flash';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import httpStatus from '~/lib/utils/http_status';
......@@ -99,7 +100,7 @@ export const receiveStageMedianValuesError = ({ commit }, error) => {
};
const fetchStageMedian = ({ groupId, valueStreamId, stageId, params }) =>
Api.cycleAnalyticsStageMedian({ groupId, valueStreamId, stageId, params }).then(({ data }) => {
getValueStreamStageMedian({ groupId, valueStreamId, stageId }, params).then(({ data }) => {
return {
id: stageId,
...(data?.error
......
......@@ -169,12 +169,6 @@ export default {
return axios.get(url, { params });
},
cycleAnalyticsStageMedian({ groupId, valueStreamId, stageId, params = {} }) {
const stageBase = this.cycleAnalyticsStageUrl({ groupId, valueStreamId, stageId });
const url = `${stageBase}/median`;
return axios.get(url, { params });
},
cycleAnalyticsStageCount({ groupId, valueStreamId, stageId, params = {} }) {
const stageBase = this.cycleAnalyticsStageUrl({ groupId, valueStreamId, stageId });
const url = `${stageBase}/count`;
......
......@@ -455,29 +455,6 @@ describe('Api', () => {
});
});
describe('cycleAnalyticsStageMedian', () => {
it('fetches stage events', (done) => {
const response = { value: '5 days ago' };
const params = { ...defaultParams };
const expectedUrl = valueStreamBaseUrl({
id: valueStreamId,
resource: `stages/${stageId}/median`,
});
mock.onGet(expectedUrl).reply(httpStatus.OK, response);
Api.cycleAnalyticsStageMedian({ groupId, valueStreamId, stageId, params })
.then((responseObj) =>
expectRequestWithCorrectParameters(responseObj, {
response,
params,
expectedUrl,
}),
)
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsDurationChart', () => {
it('fetches stage duration data', (done) => {
const response = [];
......
......@@ -174,6 +174,15 @@ export const stageMedians = {
staging: 388800,
};
export const formattedStageMedians = {
issue: '2d',
plan: '1d',
review: '1w',
code: '1d',
test: '3d',
staging: '4d',
};
export const allowedStages = [issueStage, planStage, codeStage];
export const transformedProjectStagePathData = [
......
......@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import httpStatusCodes from '~/lib/utils/http_status';
import { selectedStage, selectedValueStream } from '../mock_data';
import { allowedStages, selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
......@@ -25,6 +25,10 @@ const mockRequestedDataMutations = [
},
];
const features = {
cycleAnalyticsForGroups: true,
};
describe('Project Value Stream Analytics actions', () => {
let state;
let mock;
......@@ -175,6 +179,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
features,
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
......@@ -182,6 +187,29 @@ describe('Project Value Stream Analytics actions', () => {
});
it(`commits the 'REQUEST_VALUE_STREAMS' mutation`, () =>
testAction({
action: actions.fetchValueStreams,
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [
{ type: 'receiveValueStreamsSuccess' },
{ type: 'setSelectedStage' },
{ type: 'fetchStageMedians' },
],
}));
describe('with cycleAnalyticsForGroups=false', () => {
beforeEach(() => {
state = {
features: { cycleAnalyticsForGroups: false },
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
});
it("does not dispatch the 'fetchStageMedians' request", () =>
testAction({
action: actions.fetchValueStreams,
state,
......@@ -189,6 +217,7 @@ describe('Project Value Stream Analytics actions', () => {
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
}));
});
describe('with a failing request', () => {
beforeEach(() => {
......@@ -280,4 +309,59 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
describe('fetchStageMedians', () => {
const mockValueStreamPath = /median/;
const stageMediansPayload = [
{ id: 'issue', value: null },
{ id: 'plan', value: null },
{ id: 'code', value: null },
];
const stageMedianError = new Error(
`Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
);
beforeEach(() => {
state = {
fullPath: mockFullPath,
selectedValueStream,
stages: allowedStages,
};
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
});
it(`commits the 'REQUEST_STAGE_MEDIANS' and 'RECEIVE_STAGE_MEDIANS_SUCCESS' mutations`, () =>
testAction({
action: actions.fetchStageMedians,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_STAGE_MEDIANS' },
{ type: 'RECEIVE_STAGE_MEDIANS_SUCCESS', payload: stageMediansPayload },
],
expectedActions: [],
}));
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
testAction({
action: actions.fetchStageMedians,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_STAGE_MEDIANS' },
{ type: 'RECEIVE_STAGE_MEDIANS_ERROR', payload: stageMedianError },
],
expectedActions: [],
}));
});
});
});
import { useFakeDate } from 'helpers/fake_date';
import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants';
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import {
......@@ -9,15 +11,23 @@ import {
selectedValueStream,
rawValueStreamStages,
valueStreamStages,
rawStageMedians,
formattedStageMedians,
} from '../mock_data';
let state;
const mockRequestPath = 'fake/request/path';
const mockStartData = '2021-04-20';
const mockCreatedAfter = '2020-06-18';
const mockCreatedBefore = '2020-07-18';
const features = {
cycleAnalyticsForGroups: true,
};
describe('Project Value Stream Analytics mutations', () => {
useFakeDate(2020, 6, 18);
beforeEach(() => {
state = {};
state = { features };
});
afterEach(() => {
......@@ -46,6 +56,8 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'hasError'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, {});
......@@ -55,13 +67,17 @@ describe('Project Value Stream Analytics mutations', () => {
it.each`
mutation | payload | stateKey | value
${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'startDate'} | ${DEFAULT_DAYS_TO_DISPLAY}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
......@@ -92,4 +108,35 @@ describe('Project Value Stream Analytics mutations', () => {
},
);
});
describe('with cycleAnalyticsForGroups=false', () => {
useFakeDate(2020, 6, 18);
beforeEach(() => {
state = { features: { cycleAnalyticsForGroups: false } };
});
const formattedMedians = {
code: '2d',
issue: '-',
plan: '21h',
review: '-',
staging: '2d',
test: '4h',
};
it.each`
mutation | payload | stateKey | value
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'medians'} | ${formattedMedians}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${{}} | ${'medians'} | ${{}}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${{}} | ${'medians'} | ${{}}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
mutations[mutation](state, payload);
expect(state).toMatchObject({ [stateKey]: value });
},
);
});
});
import { useFakeDate } from 'helpers/fake_date';
import {
decorateEvents,
decorateData,
......@@ -6,6 +7,7 @@ import {
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
calculateFormattedDayInPast,
} from '~/cycle_analytics/utils';
import {
selectedStage,
......@@ -149,4 +151,12 @@ describe('Value stream analytics utils', () => {
expect(filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result);
});
});
describe('calculateFormattedDayInPast', () => {
useFakeDate(1815, 11, 10);
it('will return 2 dates, now and past', () => {
expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment