Commit cd4c047c authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Frédéric Caplette

Implement deep linking for project VSA filters

Synchronizes the url search parameters
with the relevant values in the vuex store.

Pass search parameters through to vuex

Set the date range from query parameters

Added feature spec for deep linking

Adds additional feature specs to test deep linking
directly to a project value stream.

Ensure we reload a value stream for projects

Minor clean up

Changelog: added
parent 13a6ac1f
......@@ -2,10 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
......@@ -19,6 +21,7 @@ export default {
StageTable,
ValueStreamFilters,
ValueStreamMetrics,
UrlSync,
},
props: {
noDataSvgPath: {
......@@ -54,6 +57,9 @@ export default {
'pagination',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
isLoaded() {
return !this.isLoading && !this.isLoadingStage;
},
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
......@@ -98,6 +104,16 @@ export default {
metricsRequests() {
return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
},
query() {
return {
created_after: toYmd(this.createdAfter),
created_before: toYmd(this.createdBefore),
stage_id: this.selectedStage?.id || null,
sort: this.pagination?.sort || null,
direction: this.pagination?.direction || null,
page: this.pagination?.page || null,
};
},
},
methods: {
...mapActions([
......@@ -176,5 +192,6 @@ export default {
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/>
<url-sync v-if="isLoaded" :query="query" />
</div>
</template>
......@@ -5,8 +5,6 @@ import {
} from '~/api/analytics_api';
import { __, s__ } from '~/locale';
export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
......
import Vue from 'vue';
import {
extractFilterQueryParameters,
extractPaginationQueryParameters,
} from '~/analytics/shared/utils';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
import { calculateFormattedDayInPast } from './utils';
import { buildCycleAnalyticsInitialData } from './utils';
Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
const {
noAccessSvgPath,
noDataSvgPath,
requestPath,
fullPath,
projectId,
groupId,
groupPath,
labelsPath,
milestonesPath,
} = el.dataset;
const { noAccessSvgPath, noDataSvgPath } = el.dataset;
const initialData = buildCycleAnalyticsInitialData({ ...el.dataset, gon });
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
const pagination = extractPaginationQueryParameters(window.location.search);
const {
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
} = extractFilterQueryParameters(window.location.search);
store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10),
endpoints: {
requestPath,
fullPath,
labelsPath,
milestonesPath,
groupId: parseInt(groupId, 10),
groupPath,
},
features: {
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
},
createdBefore: new Date(now),
createdAfter: new Date(past),
...initialData,
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
pagination,
});
// eslint-disable-next-line no-new
......@@ -52,7 +44,6 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
fullPath,
},
}),
});
......
......@@ -14,7 +14,7 @@ import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
return dispatch('fetchValueStreamStages');
};
export const fetchValueStreamStages = ({ commit, state }) => {
......@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state;
commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
......@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({
});
};
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
return dispatch('fetchStageData');
};
export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
const refetchStageData = (dispatch) => {
return Promise.resolve()
.then(() => dispatch('setLoading', true))
.then(() =>
export const fetchValueStreamStageData = ({ dispatch }) =>
Promise.all([
dispatch('fetchCycleAnalyticsData'),
dispatch('fetchStageData'),
dispatch('fetchStageMedians'),
dispatch('fetchStageCountValues'),
]),
)
.finally(() => dispatch('setLoading', false));
]);
export const refetchStageData = async ({ dispatch, commit }) => {
commit(types.SET_LOADING, true);
await dispatch('fetchValueStreamStageData');
commit(types.SET_LOADING, false);
};
export const setSelectedStage = ({ dispatch, commit }, selectedStage = null) => {
commit(types.SET_SELECTED_STAGE, selectedStage);
return dispatch('refetchStageData');
};
export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
export const setFilters = ({ dispatch }) => dispatch('refetchStageData');
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
return refetchStageData(dispatch);
return dispatch('refetchStageData');
};
export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
const selectedStage = stage || stages[0];
commit(types.SET_SELECTED_STAGE, selectedStage);
return dispatch('fetchValueStreamStageData');
};
export const updateStageTablePagination = (
......@@ -190,12 +191,18 @@ export const updateStageTablePagination = (
return dispatch('fetchStageData', selectedStage.id);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
selectedStage = null,
} = initialData;
dispatch('filters/setEndpoints', {
labelsEndpoint: labelsPath,
milestonesEndpoint: milestonesPath,
......@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
projectEndpoint: fullPath,
});
return dispatch('setLoading', true)
.then(() => dispatch('fetchValueStreams'))
.finally(() => dispatch('setLoading', false));
dispatch('filters/initialize', {
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
});
commit(types.SET_LOADING, true);
await dispatch('fetchValueStreams');
await dispatch('setInitialStage', selectedStage);
commit(types.SET_LOADING, false);
};
import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility';
......@@ -74,23 +71,6 @@ 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)),
};
};
/**
* @typedef {Object} MetricData
* @property {String} title - Title of the metric measured
......@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '',
};
});
const extractFeatures = (gon) => ({
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
});
/**
* Builds the initial data object for Value Stream Analytics with data loaded from the backend
*
* @param {Object} dataset - dataset object paseed to the frontend via data-* properties
* @returns {Object} - The initial data to load the app with
*/
export const buildCycleAnalyticsInitialData = ({
fullPath,
requestPath,
projectId,
groupId,
groupPath,
labelsPath,
milestonesPath,
stage,
createdAfter,
createdBefore,
gon,
} = {}) => {
return {
projectId: parseInt(projectId, 10),
endpoints: {
requestPath,
fullPath,
labelsPath,
milestonesPath,
groupId: parseInt(groupId, 10),
groupPath,
},
createdAfter: new Date(createdAfter),
createdBefore: new Date(createdBefore),
selectedStage: stage ? JSON.parse(stage) : null,
features: extractFeatures(gon),
};
};
......@@ -6,8 +6,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include CycleAnalyticsParams
include GracefulTimeoutHandling
include RedisTracking
extend ::Gitlab::Utils::Override
before_action :authorize_read_cycle_analytics!
before_action :load_value_stream, only: :show
track_redis_hll_event :show, name: 'p_analytics_valuestream'
......@@ -19,6 +21,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show
@cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params))
@request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
respond_to do |format|
format.html do
......@@ -34,6 +37,15 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
override :all_cycle_analytics_params
def all_cycle_analytics_params
super.merge({ project: @project, value_stream: @value_stream })
end
def load_value_stream
@value_stream = Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
end
def cycle_analytics_json
{
summary: @cycle_analytics.summary,
......
- page_title _("Value Stream Analytics")
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- data_attributes.merge!(cycle_analytics_initial_data(@project, @group))
- add_page_specific_style 'page_bundles/cycle_analytics'
#js-cycle-analytics{ data: cycle_analytics_initial_data(@project, @group) }
#js-cycle-analytics{ data: data_attributes }
......@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
let_it_be(:stage_filter_bar) { '[data-testid="vsa-filter-bar"]' }
let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' }
let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' }
let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' }
......@@ -27,6 +28,9 @@ RSpec.describe 'Value Stream Analytics', :js do
def set_daterange(from_date, to_date)
page.find(".js-daterange-picker-from input").set(from_date)
page.find(".js-daterange-picker-to input").set(to_date)
# simulate a blur event
page.find(".js-daterange-picker-to input").send_keys(:tab)
wait_for_all_requests
end
......@@ -158,6 +162,18 @@ RSpec.describe 'Value Stream Analytics', :js do
expect(page).not_to have_text(original_first_title, exact: true)
end
it 'can navigate directly to a value stream stream stage with filters applied' do
visit project_cycle_analytics_path(project, created_before: '2019-12-31', created_after: '2019-11-01', stage_id: 'code', milestone_title: milestone.title)
wait_for_requests
expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Code')
expect(page.find(".js-daterange-picker-from input").value).to eq("2019-11-01")
expect(page.find(".js-daterange-picker-to input").value).to eq("2019-12-31")
filter_bar = page.find(stage_filter_bar)
expect(filter_bar.find(".gl-filtered-search-token-data-content").text).to eq("%#{milestone.title}")
end
def stage_time_column
stage_table.find(stage_table_duration_column_header_selector).ancestor("th")
end
......
......@@ -9,7 +9,6 @@ import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_
import { TEST_HOST } from 'helpers/test_constants';
import {
DEFAULT_VALUE_STREAM,
DEFAULT_DAYS_IN_PAST,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
......@@ -17,6 +16,7 @@ import {
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
const DEFAULT_DAYS_IN_PAST = 30;
export const createdBefore = new Date(2019, 0, 14);
export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST);
......
......@@ -57,22 +57,12 @@ describe('Project Value Stream Analytics actions', () => {
const mutationTypes = (arr) => arr.map(({ type }) => type);
const mockFetchStageDataActions = [
{ type: 'setLoading', payload: true },
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
{ type: 'setLoading', payload: false },
];
describe.each`
action | payload | expectedActions | expectedMutations
${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
......@@ -86,9 +76,18 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('initializeVsa', () => {
let mockDispatch;
let mockCommit;
const payload = { endpoints: mockEndpoints };
const selectedAuthor = 'Author';
const selectedMilestone = 'Milestone 1';
const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
const selectedLabelList = ['Label 1', 'Label 2'];
const payload = {
endpoints: mockEndpoints,
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
selectedLabelList,
selectedStage,
};
const mockFilterEndpoints = {
groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath,
......@@ -96,27 +95,63 @@ describe('Project Value Stream Analytics actions', () => {
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
};
it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
return testAction({
action: actions.initializeVsa,
state: {},
payload,
expectedMutations: [
{ type: 'INITIALIZE_VSA', payload },
{ type: 'SET_LOADING', payload: true },
{ type: 'SET_LOADING', payload: false },
],
expectedActions: [
{ type: 'filters/setEndpoints', payload: mockFilterEndpoints },
{
type: 'filters/initialize',
payload: { selectedAuthor, selectedMilestone, selectedAssigneeList, selectedLabelList },
},
{ type: 'fetchValueStreams' },
{ type: 'setInitialStage', payload: selectedStage },
],
});
});
});
describe('setInitialStage', () => {
beforeEach(() => {
mockDispatch = jest.fn(() => Promise.resolve());
mockCommit = jest.fn();
state = { ...state, stages: allowedStages };
});
it('will dispatch the setLoading and fetchValueStreams actions and commit INITIALIZE_VSA', async () => {
await actions.initializeVsa(
describe('with a selected stage', () => {
it('will commit `SET_SELECTED_STAGE` and fetchValueStreamStageData actions', () => {
const fakeStage = { ...selectedStage, id: 'fake', name: 'fake-stae' };
return testAction({
action: actions.setInitialStage,
state,
payload: fakeStage,
expectedMutations: [
{
...state,
dispatch: mockDispatch,
commit: mockCommit,
type: 'SET_SELECTED_STAGE',
payload: fakeStage,
},
payload,
);
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
],
expectedActions: [{ type: 'fetchValueStreamStageData' }],
});
});
});
expect(mockDispatch).toHaveBeenCalledTimes(4);
expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints);
expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
describe('without a selected stage', () => {
it('will select the first stage from the value stream', () => {
const [firstStage] = allowedStages;
testAction({
action: actions.setInitialStage,
state,
payload: null,
expectedMutations: [{ type: 'SET_SELECTED_STAGE', payload: firstStage }],
expectedActions: [{ type: 'fetchValueStreamStageData' }],
});
});
});
});
......@@ -270,12 +305,7 @@ describe('Project Value Stream Analytics actions', () => {
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [
{ type: 'receiveValueStreamsSuccess' },
{ type: 'setSelectedStage' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
],
expectedActions: [{ type: 'receiveValueStreamsSuccess' }],
}));
describe('with a failing request', () => {
......@@ -483,4 +513,34 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
describe('refetchStageData', () => {
it('will commit SET_LOADING and dispatch fetchValueStreamStageData actions', () =>
testAction({
action: actions.refetchStageData,
state,
payload: {},
expectedMutations: [
{ type: 'SET_LOADING', payload: true },
{ type: 'SET_LOADING', payload: false },
],
expectedActions: [{ type: 'fetchValueStreamStageData' }],
}));
});
describe('fetchValueStreamStageData', () => {
it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
testAction({
action: actions.fetchValueStreamStageData,
state,
payload: {},
expectedMutations: [],
expectedActions: [
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
],
}));
});
});
......@@ -101,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
......
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import { useFakeDate } from 'helpers/fake_date';
import {
transformStagesForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
calculateFormattedDayInPast,
prepareTimeMetricsData,
buildCycleAnalyticsInitialData,
} from '~/cycle_analytics/utils';
import { slugify } from '~/lib/utils/text_utility';
import {
......@@ -90,14 +89,6 @@ describe('Value stream analytics utils', () => {
});
});
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' });
});
});
describe('prepareTimeMetricsData', () => {
let prepared;
const [first, second] = metricsData;
......@@ -125,4 +116,87 @@ describe('Value stream analytics utils', () => {
]);
});
});
describe('buildCycleAnalyticsInitialData', () => {
let res = null;
const projectId = '5';
const createdAfter = '2021-09-01';
const createdBefore = '2021-11-06';
const groupId = '146';
const groupPath = 'fake-group';
const fullPath = 'fake-group/fake-project';
const labelsPath = '/fake-group/fake-project/-/labels.json';
const milestonesPath = '/fake-group/fake-project/-/milestones.json';
const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
const rawData = {
projectId,
createdBefore,
createdAfter,
fullPath,
requestPath,
labelsPath,
milestonesPath,
groupId,
groupPath,
};
describe('with minimal data', () => {
beforeEach(() => {
res = buildCycleAnalyticsInitialData(rawData);
});
it('sets the projectId', () => {
expect(res.projectId).toBe(parseInt(projectId, 10));
});
it('sets the date range', () => {
expect(res.createdBefore).toEqual(new Date(createdBefore));
expect(res.createdAfter).toEqual(new Date(createdAfter));
});
it('sets the endpoints', () => {
const { endpoints } = res;
expect(endpoints.fullPath).toBe(fullPath);
expect(endpoints.requestPath).toBe(requestPath);
expect(endpoints.labelsPath).toBe(labelsPath);
expect(endpoints.milestonesPath).toBe(milestonesPath);
expect(endpoints.groupId).toBe(parseInt(groupId, 10));
expect(endpoints.groupPath).toBe(groupPath);
});
it('returns null when there is no stage', () => {
expect(res.selectedStage).toBeNull();
});
it('returns false for missing features', () => {
expect(res.features.cycleAnalyticsForGroups).toBe(false);
});
});
describe('with a stage set', () => {
const jsonStage = '{"id":"fakeStage","title":"fakeStage"}';
it('parses the selectedStage data', () => {
res = buildCycleAnalyticsInitialData({ ...rawData, stage: jsonStage });
const { selectedStage: stage } = res;
expect(stage.id).toBe('fakeStage');
expect(stage.title).toBe('fakeStage');
});
});
describe('with features set', () => {
const fakeFeatures = { cycleAnalyticsForGroups: true };
it('sets the feature flags', () => {
res = buildCycleAnalyticsInitialData({
...rawData,
gon: { licensed_features: fakeFeatures },
});
expect(res.features).toEqual(fakeFeatures);
});
});
});
});
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