diff options
Diffstat (limited to 'spec/frontend/cycle_analytics')
-rw-r--r-- | spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap | 28 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/base_spec.js | 265 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/filter_bar_spec.js | 223 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/formatted_stage_count_spec.js | 34 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/mock_data.js | 261 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/path_navigation_spec.js | 150 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/stage_table_spec.js | 371 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/store/actions_spec.js | 518 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/store/getters_spec.js | 42 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/store/mutations_spec.js | 132 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/total_time_spec.js | 45 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/utils_spec.js | 171 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/value_stream_filters_spec.js | 91 | ||||
-rw-r--r-- | spec/frontend/cycle_analytics/value_stream_metrics_spec.js | 185 |
14 files changed, 0 insertions, 2516 deletions
diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap deleted file mode 100644 index 92927ef16ec..00000000000 --- a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TotalTime with a blank object should render -- 1`] = `"<span> -- </span>"`; - -exports[`TotalTime with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = ` -"<span> - 3 <span>days</span></span>" -`; - -exports[`TotalTime with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = ` -"<span> - 7 <span>hrs</span></span>" -`; - -exports[`TotalTime with a valid time object with {"hours": 23, "mins": 10} 1`] = ` -"<span> - 23 <span>hrs</span></span>" -`; - -exports[`TotalTime with a valid time object with {"mins": 47, "seconds": 3} 1`] = ` -"<span> - 47 <span>mins</span></span>" -`; - -exports[`TotalTime with a valid time object with {"seconds": 35} 1`] = ` -"<span> - 35 <span>s</span></span>" -`; diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js deleted file mode 100644 index 013bea671a8..00000000000 --- a/spec/frontend/cycle_analytics/base_spec.js +++ /dev/null @@ -1,265 +0,0 @@ -import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; -import BaseComponent from '~/cycle_analytics/components/base.vue'; -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 { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; -import initState from '~/cycle_analytics/store/state'; -import { - transformedProjectStagePathData, - selectedStage, - issueEvents, - createdBefore, - createdAfter, - currentGroup, - stageCounts, - initialPaginationState as pagination, -} from './mock_data'; - -const selectedStageEvents = issueEvents.events; -const noDataSvgPath = 'path/to/no/data'; -const noAccessSvgPath = 'path/to/no/access'; -const selectedStageCount = stageCounts[selectedStage.id]; -const fullPath = 'full/path/to/foo'; - -Vue.use(Vuex); - -let wrapper; - -const { id: groupId, path: groupPath } = currentGroup; -const defaultState = { - currentGroup, - createdBefore, - createdAfter, - stageCounts, - endpoints: { fullPath, groupId, groupPath }, -}; - -function createStore({ initialState = {}, initialGetters = {} }) { - return new Vuex.Store({ - state: { - ...initState(), - ...defaultState, - ...initialState, - }, - getters: { - pathNavigationData: () => transformedProjectStagePathData, - filterParams: () => ({ - created_after: createdAfter, - created_before: createdBefore, - }), - ...initialGetters, - }, - }); -} - -function createComponent({ initialState, initialGetters } = {}) { - return extendedWrapper( - shallowMount(BaseComponent, { - store: createStore({ initialState, initialGetters }), - propsData: { - noDataSvgPath, - noAccessSvgPath, - }, - stubs: { - StageTable, - }, - }), - ); -} - -const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); -const findPathNavigation = () => wrapper.findComponent(PathNavigation); -const findFilters = () => wrapper.findComponent(ValueStreamFilters); -const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics); -const findStageTable = () => wrapper.findComponent(StageTable); -const findStageEvents = () => findStageTable().props('stageEvents'); -const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title'); -const findPagination = () => wrapper.findByTestId('vsa-stage-pagination'); - -const hasMetricsRequests = (reqs) => { - const foundReqs = findOverviewMetrics().props('requests'); - expect(foundReqs.length).toEqual(reqs.length); - expect(foundReqs.map(({ name }) => name)).toEqual(reqs); -}; - -describe('Value stream analytics component', () => { - beforeEach(() => { - wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } }); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('renders the path navigation component', () => { - expect(findPathNavigation().exists()).toBe(true); - }); - - it('receives the stages formatted for the path navigation', () => { - expect(findPathNavigation().props('stages')).toBe(transformedProjectStagePathData); - }); - - it('renders the overview metrics', () => { - expect(findOverviewMetrics().exists()).toBe(true); - }); - - it('passes requests prop to the metrics component', () => { - hasMetricsRequests(['recent activity']); - }); - - it('renders the stage table', () => { - expect(findStageTable().exists()).toBe(true); - }); - - it('passes the selected stage count to the stage table', () => { - expect(findStageTable().props('stageCount')).toBe(selectedStageCount); - }); - - it('renders the stage table events', () => { - expect(findStageEvents()).toEqual(selectedStageEvents); - }); - - it('renders the filters', () => { - expect(findFilters().exists()).toBe(true); - }); - - it('displays the date range selector and hides the project selector', () => { - expect(findFilters().props()).toMatchObject({ - hasProjectFilter: false, - hasDateRangeFilter: true, - }); - }); - - it('passes the paths to the filter bar', () => { - expect(findFilters().props()).toEqual({ - groupId, - groupPath, - endDate: createdBefore, - hasDateRangeFilter: true, - hasProjectFilter: false, - selectedProjects: [], - startDate: createdAfter, - }); - }); - - it('does not render the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('renders pagination', () => { - expect(findPagination().exists()).toBe(true); - }); - - describe('with `cycleAnalyticsForGroups=true` license', () => { - beforeEach(() => { - wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } }); - }); - - it('passes requests prop to the metrics component', () => { - hasMetricsRequests(['time summary', 'recent activity']); - }); - }); - - describe('isLoading = true', () => { - beforeEach(() => { - wrapper = createComponent({ - initialState: { isLoading: true }, - }); - }); - - it('renders the path navigation component with prop `loading` set to true', () => { - expect(findPathNavigation().props('loading')).toBe(true); - }); - - it('does not render the stage table', () => { - expect(findStageTable().exists()).toBe(false); - }); - - it('renders the overview metrics', () => { - expect(findOverviewMetrics().exists()).toBe(true); - }); - - it('renders the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('isLoadingStage = true', () => { - beforeEach(() => { - wrapper = createComponent({ - initialState: { isLoadingStage: true }, - }); - }); - - it('renders the stage table with a loading icon', () => { - const tableWrapper = findStageTable(); - expect(tableWrapper.exists()).toBe(true); - expect(tableWrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('renders the path navigation loading state', () => { - expect(findPathNavigation().props('loading')).toBe(true); - }); - }); - - describe('isEmptyStage = true', () => { - const emptyStageParams = { - isEmptyStage: true, - selectedStage: { ...selectedStage, emptyStageText: 'This stage is empty' }, - }; - beforeEach(() => { - wrapper = createComponent({ initialState: emptyStageParams }); - }); - - it('renders the empty stage with `Not enough data` message', () => { - expect(findEmptyStageTitle()).toBe(NOT_ENOUGH_DATA_ERROR); - }); - - describe('with a selectedStageError', () => { - beforeEach(() => { - wrapper = createComponent({ - initialState: { - ...emptyStageParams, - selectedStageError: 'There is too much data to calculate', - }, - }); - }); - - it('renders the empty stage with `There is too much data to calculate` message', () => { - expect(findEmptyStageTitle()).toBe('There is too much data to calculate'); - }); - }); - }); - - describe('without a selected stage', () => { - beforeEach(() => { - wrapper = createComponent({ - initialGetters: { pathNavigationData: () => [] }, - initialState: { selectedStage: null, isEmptyStage: true }, - }); - }); - - it('renders the stage table', () => { - expect(findStageTable().exists()).toBe(true); - }); - - it('does not render the path navigation', () => { - expect(findPathNavigation().exists()).toBe(false); - }); - - it('does not render the stage table events', () => { - expect(findStageEvents()).toHaveLength(0); - }); - - it('does not render the loading icon', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/cycle_analytics/filter_bar_spec.js deleted file mode 100644 index 36933790cf7..00000000000 --- a/spec/frontend/cycle_analytics/filter_bar_spec.js +++ /dev/null @@ -1,223 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import Vuex from 'vuex'; -import { - filterMilestones, - filterLabels, -} from 'jest/vue_shared/components/filtered_search_bar/store/modules/filters/mock_data'; -import FilterBar from '~/cycle_analytics/components/filter_bar.vue'; -import storeConfig from '~/cycle_analytics/store'; -import * as commonUtils from '~/lib/utils/common_utils'; -import * as urlUtils from '~/lib/utils/url_utility'; -import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; -import initialFiltersState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state'; -import UrlSync from '~/vue_shared/components/url_sync.vue'; - -Vue.use(Vuex); - -const milestoneTokenType = 'milestone'; -const labelsTokenType = 'labels'; -const authorTokenType = 'author'; -const assigneesTokenType = 'assignees'; - -const initialFilterBarState = { - selectedMilestone: null, - selectedAuthor: null, - selectedAssigneeList: null, - selectedLabelList: null, -}; - -const defaultParams = { - milestone_title: null, - 'not[milestone_title]': null, - author_username: null, - 'not[author_username]': null, - assignee_username: null, - 'not[assignee_username]': null, - label_name: null, - 'not[label_name]': null, -}; - -async function shouldMergeUrlParams(wrapper, result) { - await nextTick(); - expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, { - spreadArrays: true, - }); - expect(commonUtils.historyPushState).toHaveBeenCalled(); -} - -describe('Filter bar', () => { - let wrapper; - let store; - let mock; - - let setFiltersMock; - - const createStore = (initialState = {}) => { - setFiltersMock = jest.fn(); - - return new Vuex.Store({ - modules: { - filters: { - namespaced: true, - state: { - ...initialFiltersState(), - ...initialState, - }, - actions: { - setFilters: setFiltersMock, - }, - }, - }, - }); - }; - - const createComponent = (initialStore) => { - return shallowMount(FilterBar, { - store: initialStore, - propsData: { - groupPath: 'foo', - }, - stubs: { - UrlSync, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - wrapper.destroy(); - mock.restore(); - }); - - const selectedMilestone = [filterMilestones[0]]; - const selectedLabelList = [filterLabels[0]]; - - const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBar); - const getSearchToken = (type) => - findFilteredSearch() - .props('tokens') - .find((token) => token.type === type); - - describe('default', () => { - beforeEach(() => { - store = createStore(); - wrapper = createComponent(store); - }); - - it('renders FilteredSearchBar component', () => { - expect(findFilteredSearch().exists()).toBe(true); - }); - }); - - describe('when the state has data', () => { - beforeEach(() => { - store = createStore({ - milestones: { data: selectedMilestone }, - labels: { data: selectedLabelList }, - authors: { data: [] }, - assignees: { data: [] }, - }); - wrapper = createComponent(store); - }); - - it('displays the milestone and label token', () => { - const tokens = findFilteredSearch().props('tokens'); - - expect(tokens).toHaveLength(4); - expect(tokens[0].type).toBe(milestoneTokenType); - expect(tokens[1].type).toBe(labelsTokenType); - expect(tokens[2].type).toBe(authorTokenType); - expect(tokens[3].type).toBe(assigneesTokenType); - }); - - it('provides the initial milestone token', () => { - const { initialMilestones: milestoneToken } = getSearchToken(milestoneTokenType); - - expect(milestoneToken).toHaveLength(selectedMilestone.length); - }); - - it('provides the initial label token', () => { - const { initialLabels: labelToken } = getSearchToken(labelsTokenType); - - expect(labelToken).toHaveLength(selectedLabelList.length); - }); - }); - - describe('when the user interacts', () => { - beforeEach(() => { - store = createStore({ - milestones: { data: filterMilestones }, - labels: { data: filterLabels }, - }); - wrapper = createComponent(store); - jest.spyOn(utils, 'processFilters'); - }); - - it('clicks on the search button, setFilters is dispatched', () => { - const filters = [ - { type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } }, - { type: 'labels', value: { data: selectedLabelList[0].title, operator: '=' } }, - ]; - - findFilteredSearch().vm.$emit('onFilter', filters); - - expect(utils.processFilters).toHaveBeenCalledWith(filters); - - expect(setFiltersMock).toHaveBeenCalledWith(expect.anything(), { - selectedLabelList: [{ value: selectedLabelList[0].title, operator: '=' }], - selectedMilestone: { value: selectedMilestone[0].title, operator: '=' }, - selectedAssigneeList: [], - selectedAuthor: null, - }); - }); - }); - - describe.each([ - ['selectedMilestone', 'milestone_title', { value: '12.0', operator: '=' }, '12.0'], - ['selectedAuthor', 'author_username', { value: 'rootUser', operator: '=' }, 'rootUser'], - [ - 'selectedLabelList', - 'label_name', - [ - { value: 'Afternix', operator: '=' }, - { value: 'Brouceforge', operator: '=' }, - ], - ['Afternix', 'Brouceforge'], - ], - [ - 'selectedAssigneeList', - 'assignee_username', - [ - { value: 'rootUser', operator: '=' }, - { value: 'secondaryUser', operator: '=' }, - ], - ['rootUser', 'secondaryUser'], - ], - ])('with a %s updates the %s url parameter', (stateKey, paramKey, payload, result) => { - beforeEach(() => { - commonUtils.historyPushState = jest.fn(); - urlUtils.mergeUrlParams = jest.fn(); - - mock = new MockAdapter(axios); - wrapper = createComponent(storeConfig); - - wrapper.vm.$store.dispatch('filters/setFilters', { - ...initialFilterBarState, - [stateKey]: payload, - }); - }); - it(`sets the ${paramKey} url parameter`, () => { - return shouldMergeUrlParams(wrapper, { - ...defaultParams, - [paramKey]: result, - }); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/cycle_analytics/formatted_stage_count_spec.js deleted file mode 100644 index 1228b8511ea..00000000000 --- a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Component from '~/cycle_analytics/components/formatted_stage_count.vue'; - -describe('Formatted Stage Count', () => { - let wrapper = null; - - const createComponent = (stageCount = null) => { - wrapper = shallowMount(Component, { - propsData: { - stageCount, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it.each` - stageCount | expectedOutput - ${null} | ${'-'} - ${1} | ${'1 item'} - ${10} | ${'10 items'} - ${1000} | ${'1,000 items'} - ${1001} | ${'1,000+ items'} - `('returns "$expectedOutput" for stageCount=$stageCount', ({ stageCount, expectedOutput }) => { - createComponent(stageCount); - expect(wrapper.text()).toContain(expectedOutput); - }); -}); diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js deleted file mode 100644 index 02666260cdb..00000000000 --- a/spec/frontend/cycle_analytics/mock_data.js +++ /dev/null @@ -1,261 +0,0 @@ -import valueStreamAnalyticsStages from 'test_fixtures/projects/analytics/value_stream_analytics/stages.json'; -import issueStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/issue.json'; -import planStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/plan.json'; -import reviewStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/review.json'; -import codeStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/code.json'; -import testStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/test.json'; -import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/staging.json'; - -import { TEST_HOST } from 'helpers/test_constants'; -import { - DEFAULT_VALUE_STREAM, - PAGINATION_TYPE, - PAGINATION_SORT_DIRECTION_DESC, - PAGINATION_SORT_FIELD_END_EVENT, -} from '~/cycle_analytics/constants'; -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); - -export const deepCamelCase = (obj) => convertObjectPropsToCamelCase(obj, { deep: true }); - -export const getStageByTitle = (stages, title) => - stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {}; - -export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging']; - -const stageFixtures = { - issue: issueStageFixtures, - plan: planStageFixtures, - review: reviewStageFixtures, - code: codeStageFixtures, - test: testStageFixtures, - staging: stagingStageFixtures, -}; - -export const summary = [ - { value: '20', title: 'New Issues' }, - { value: null, title: 'Commits' }, - { value: null, title: 'Deploys' }, - { value: null, title: 'Deployment Frequency', unit: '/day' }, -]; - -export const issueStage = { - id: 'issue', - title: 'Issue', - name: 'issue', - legend: '', - description: 'Time before an issue gets scheduled', - value: null, -}; - -export const planStage = { - id: 'plan', - title: 'Plan', - name: 'plan', - legend: '', - description: 'Time before an issue starts implementation', - value: 75600, -}; - -export const codeStage = { - id: 'code', - title: 'Code', - name: 'code', - legend: '', - description: 'Time until first merge request', - value: 172800, -}; - -export const testStage = { - id: 'test', - title: 'Test', - name: 'test', - legend: '', - description: 'Total test time for all commits/merges', - value: 17550, -}; - -export const reviewStage = { - id: 'review', - title: 'Review', - name: 'review', - legend: '', - description: 'Time between merge request creation and merge/close', - value: null, -}; - -export const stagingStage = { - id: 'staging', - title: 'Staging', - name: 'staging', - legend: '', - description: 'From merge request merge until deploy to production', - value: 172800, -}; - -export const selectedStage = { - ...issueStage, - value: null, - active: false, - emptyStageText: - 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - - slug: 'issue', -}; - -export const convertedData = { - summary: [ - { value: '20', title: 'New Issues' }, - { value: '-', title: 'Commits' }, - { value: '-', title: 'Deploys' }, - { value: '-', title: 'Deployment Frequency', unit: '/day' }, - ], -}; - -export const rawIssueEvents = stageFixtures.issue; -export const issueEvents = deepCamelCase(rawIssueEvents); -export const reviewEvents = deepCamelCase(stageFixtures.review); - -export const pathNavIssueMetric = 172800; - -export const rawStageCounts = [ - { id: 'issue', count: 6 }, - { id: 'plan', count: 6 }, - { id: 'code', count: 1 }, - { id: 'test', count: 5 }, - { id: 'review', count: 12 }, - { id: 'staging', count: 3 }, -]; - -export const stageCounts = { - code: 1, - issue: 6, - plan: 6, - review: 12, - staging: 3, - test: 5, -}; - -export const rawStageMedians = [ - { id: 'issue', value: 172800 }, - { id: 'plan', value: 86400 }, - { id: 'review', value: 1036800 }, - { id: 'code', value: 129600 }, - { id: 'test', value: 259200 }, - { id: 'staging', value: 388800 }, -]; - -export const stageMedians = { - issue: 172800, - plan: 86400, - review: 1036800, - code: 129600, - test: 259200, - 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 = [ - { - metric: 172800, - selected: true, - stageCount: 6, - icon: null, - id: 'issue', - title: 'Issue', - name: 'issue', - legend: '', - description: 'Time before an issue gets scheduled', - value: null, - }, - { - metric: 86400, - selected: false, - stageCount: 6, - icon: null, - id: 'plan', - title: 'Plan', - name: 'plan', - legend: '', - description: 'Time before an issue starts implementation', - value: 75600, - }, - { - metric: 129600, - selected: false, - stageCount: 1, - icon: null, - id: 'code', - title: 'Code', - name: 'code', - legend: '', - description: 'Time until first merge request', - value: 172800, - }, -]; - -export const selectedValueStream = DEFAULT_VALUE_STREAM; - -export const group = { - id: 1, - name: 'foo', - path: 'foo', - full_path: 'foo', - avatar_url: `${TEST_HOST}/images/home/nasa.svg`, -}; - -export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true }); - -export const selectedProjects = [ - { - id: 'gid://gitlab/Project/1', - name: 'cool project', - pathWithNamespace: 'group/cool-project', - avatarUrl: null, - }, - { - id: 'gid://gitlab/Project/2', - name: 'another cool project', - pathWithNamespace: 'group/another-cool-project', - avatarUrl: null, - }, -]; - -export const rawValueStreamStages = valueStreamAnalyticsStages.stages; - -export const valueStreamStages = rawValueStreamStages.map((s) => - convertObjectPropsToCamelCase(s, { deep: true }), -); - -export const initialPaginationQuery = { - page: 15, - sort: PAGINATION_SORT_FIELD_END_EVENT, - direction: PAGINATION_SORT_DIRECTION_DESC, -}; - -export const initialPaginationState = { - ...initialPaginationQuery, - page: null, - hasNextPage: false, -}; - -export const basePaginationResult = { - pagination: PAGINATION_TYPE, - sort: PAGINATION_SORT_FIELD_END_EVENT, - direction: PAGINATION_SORT_DIRECTION_DESC, - page: null, -}; diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/cycle_analytics/path_navigation_spec.js deleted file mode 100644 index fec1526359c..00000000000 --- a/spec/frontend/cycle_analytics/path_navigation_spec.js +++ /dev/null @@ -1,150 +0,0 @@ -import { GlPath, GlSkeletonLoader } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import Component from '~/cycle_analytics/components/path_navigation.vue'; -import { transformedProjectStagePathData, selectedStage } from './mock_data'; - -describe('Project PathNavigation', () => { - let wrapper = null; - let trackingSpy = null; - - const createComponent = (props) => { - return extendedWrapper( - mount(Component, { - propsData: { - stages: transformedProjectStagePathData, - selectedStage, - loading: false, - ...props, - }, - }), - ); - }; - - const findPathNavigation = () => { - return wrapper.findByTestId('gl-path-nav'); - }; - - const findPathNavigationItems = () => { - return findPathNavigation().findAll('li'); - }; - - const findPathNavigationTitles = () => { - return findPathNavigation() - .findAll('li button') - .wrappers.map((w) => w.html()); - }; - - const clickItemAt = (index) => { - findPathNavigationItems().at(index).find('button').trigger('click'); - }; - - const pathItemContent = () => findPathNavigationItems().wrappers.map(extendedWrapper); - const firstPopover = () => wrapper.findAllByTestId('stage-item-popover').at(0); - - beforeEach(() => { - wrapper = createComponent(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - wrapper.destroy(); - wrapper = null; - }); - - describe('displays correctly', () => { - it('has the correct props', () => { - expect(wrapper.findComponent(GlPath).props('items')).toMatchObject( - transformedProjectStagePathData, - ); - }); - - it('contains all the expected stages', () => { - const stageContent = findPathNavigationTitles(); - transformedProjectStagePathData.forEach((stage, index) => { - expect(stageContent[index]).toContain(stage.title); - }); - }); - - describe('loading', () => { - describe('is false', () => { - it('displays the gl-path component', () => { - expect(wrapper.findComponent(GlPath).exists()).toBe(true); - }); - - it('hides the gl-skeleton-loading component', () => { - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); - }); - - it('renders each stage', () => { - const result = findPathNavigationTitles(); - expect(result.length).toBe(transformedProjectStagePathData.length); - }); - - it('renders each stage with its median', () => { - const result = findPathNavigationTitles(); - transformedProjectStagePathData.forEach(({ title, metric }, index) => { - expect(result[index]).toContain(title); - expect(result[index]).toContain(metric.toString()); - }); - }); - - describe('popovers', () => { - beforeEach(() => { - wrapper = createComponent({ stages: transformedProjectStagePathData }); - }); - - it('renders popovers for all stages', () => { - pathItemContent().forEach((stage) => { - expect(stage.findByTestId('stage-item-popover').exists()).toBe(true); - }); - }); - - it('shows the median stage time for the first stage item', () => { - expect(firstPopover().text()).toContain('Stage time (median)'); - }); - }); - }); - - describe('is true', () => { - beforeEach(() => { - wrapper = createComponent({ loading: true }); - }); - - it('hides the gl-path component', () => { - expect(wrapper.findComponent(GlPath).exists()).toBe(false); - }); - - it('displays the gl-skeleton-loading component', () => { - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); - }); - }); - }); - }); - - describe('event handling', () => { - it('emits the selected event', () => { - expect(wrapper.emitted('selected')).toBeUndefined(); - - clickItemAt(0); - clickItemAt(1); - clickItemAt(2); - - expect(wrapper.emitted().selected).toEqual([ - [transformedProjectStagePathData[0]], - [transformedProjectStagePathData[1]], - [transformedProjectStagePathData[2]], - ]); - }); - - it('sends tracking information', () => { - clickItemAt(0); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_path_navigation', { - extra: { stage_id: selectedStage.slug }, - }); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js deleted file mode 100644 index 473e1d5b664..00000000000 --- a/spec/frontend/cycle_analytics/stage_table_spec.js +++ /dev/null @@ -1,371 +0,0 @@ -import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import StageTable from '~/cycle_analytics/components/stage_table.vue'; -import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants'; -import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data'; - -let wrapper = null; -let trackingSpy = null; - -const noDataSvgPath = 'path/to/no/data'; -const emptyStateTitle = 'Too much data'; -const notEnoughDataError = "We don't have enough data to show this stage."; -const issueEventItems = issueEvents.events; -const reviewEventItems = reviewEvents.events; -const [firstIssueEvent] = issueEventItems; -const [firstReviewEvent] = reviewEventItems; -const pagination = { page: 1, hasNextPage: true }; - -const findStageEvents = () => wrapper.findAllByTestId('vsa-stage-event'); -const findPagination = () => wrapper.findByTestId('vsa-stage-pagination'); -const findTable = () => wrapper.findComponent(GlTable); -const findTableHead = () => wrapper.find('thead'); -const findTableHeadColumns = () => findTableHead().findAll('th'); -const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title'); -const findStageEventLink = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-link'); -const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time'); -const findStageLastEvent = () => wrapper.findByTestId('vsa-stage-last-event'); -const findIcon = (name) => wrapper.findByTestId(`${name}-icon`); - -function createComponent(props = {}, shallow = false) { - const func = shallow ? shallowMount : mount; - return extendedWrapper( - func(StageTable, { - propsData: { - isLoading: false, - stageEvents: issueEventItems, - noDataSvgPath, - selectedStage: issueStage, - pagination, - ...props, - }, - stubs: { - GlLoadingIcon, - GlEmptyState, - }, - }), - ); -} - -describe('StageTable', () => { - afterEach(() => { - wrapper.destroy(); - }); - - describe('is loaded with data', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - - it('will render the correct events', () => { - const evs = findStageEvents(); - expect(evs).toHaveLength(issueEventItems.length); - - const titles = evs.wrappers.map((ev) => findStageEventTitle(ev).text()); - issueEventItems.forEach((ev, index) => { - expect(titles[index]).toBe(ev.title); - }); - }); - - it('will not display the default data message', () => { - expect(wrapper.html()).not.toContain(notEnoughDataError); - }); - }); - - describe('with minimal stage data', () => { - beforeEach(() => { - wrapper = createComponent({ currentStage: { title: 'New stage title' } }); - }); - - it('will render the correct events', () => { - const evs = findStageEvents(); - expect(evs).toHaveLength(issueEventItems.length); - - const titles = evs.wrappers.map((ev) => findStageEventTitle(ev).text()); - issueEventItems.forEach((ev, index) => { - expect(titles[index]).toBe(ev.title); - }); - }); - - it('will not display the project name in the record link', () => { - const evs = findStageEvents(); - - const links = evs.wrappers.map((ev) => findStageEventLink(ev).text()); - issueEventItems.forEach((ev, index) => { - expect(links[index]).toBe(`#${ev.iid}`); - }); - }); - }); - - describe('default event', () => { - beforeEach(() => { - wrapper = createComponent({ - stageEvents: [{ ...firstIssueEvent }], - selectedStage: { ...issueStage, custom: false }, - }); - }); - - it('will render the event title', () => { - expect(wrapper.findByTestId('vsa-stage-event-title').text()).toBe(firstIssueEvent.title); - }); - - it('will set the workflow title to "Issues"', () => { - expect(findTableHead().text()).toContain('Issues'); - }); - - it('does not render the fork icon', () => { - expect(findIcon('fork').exists()).toBe(false); - }); - - it('does not render the branch icon', () => { - expect(findIcon('commit').exists()).toBe(false); - }); - - it('will render the total time', () => { - const createdAt = firstIssueEvent.createdAt.replace(' ago', ''); - expect(findStageTime().text()).toBe(createdAt); - }); - - it('will render the end event', () => { - expect(findStageLastEvent().text()).toBe(firstIssueEvent.endEventTimestamp); - }); - - it('will render the author', () => { - expect(wrapper.findByTestId('vsa-stage-event-author').text()).toContain( - firstIssueEvent.author.name, - ); - }); - - it('will render the created at date', () => { - expect(wrapper.findByTestId('vsa-stage-event-date').text()).toContain( - firstIssueEvent.createdAt, - ); - }); - }); - - describe('merge request event', () => { - beforeEach(() => { - wrapper = createComponent({ - stageEvents: [{ ...firstReviewEvent }], - selectedStage: { ...reviewStage, custom: false }, - }); - }); - - it('will set the workflow title to "Merge requests"', () => { - expect(findTableHead().text()).toContain('Merge requests'); - expect(findTableHead().text()).not.toContain('Issues'); - }); - }); - - describe('isLoading = true', () => { - beforeEach(() => { - wrapper = createComponent({ isLoading: true }, true); - }); - - it('will display the loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('will not display pagination', () => { - expect(findPagination().exists()).toBe(false); - }); - }); - - describe('with no stageEvents', () => { - beforeEach(() => { - wrapper = createComponent({ stageEvents: [] }); - }); - - it('will render the empty state', () => { - expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); - }); - - it('will display the default no data message', () => { - expect(wrapper.html()).toContain(notEnoughDataError); - }); - - it('will not display the pagination component', () => { - expect(findPagination().exists()).toBe(false); - }); - }); - - describe('emptyStateTitle set', () => { - beforeEach(() => { - wrapper = createComponent({ stageEvents: [], emptyStateTitle }); - }); - - it('will display the custom message', () => { - expect(wrapper.html()).not.toContain(notEnoughDataError); - expect(wrapper.html()).toContain(emptyStateTitle); - }); - }); - - describe('includeProjectName set', () => { - const fakenamespace = 'some/fake/path'; - beforeEach(() => { - wrapper = createComponent({ includeProjectName: true }); - }); - - it('will display the project name in the record link', () => { - const evs = findStageEvents(); - - const links = evs.wrappers.map((ev) => findStageEventLink(ev).text()); - issueEventItems.forEach((ev, index) => { - expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`); - }); - }); - - describe.each` - namespaceFullPath | hasFullPath - ${'fake'} | ${false} - ${fakenamespace} | ${true} - `('with a namespace', ({ namespaceFullPath, hasFullPath }) => { - let evs = null; - let links = null; - - beforeEach(() => { - wrapper = createComponent({ - includeProjectName: true, - stageEvents: issueEventItems.map((ie) => ({ ...ie, namespaceFullPath })), - }); - - evs = findStageEvents(); - links = evs.wrappers.map((ev) => findStageEventLink(ev).text()); - }); - - it(`with namespaceFullPath='${namespaceFullPath}' ${ - hasFullPath ? 'will' : 'does not' - } include the namespace`, () => { - issueEventItems.forEach((ev, index) => { - if (hasFullPath) { - expect(links[index]).toBe(`${namespaceFullPath}/${ev.projectPath}#${ev.iid}`); - } else { - expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`); - } - }); - }); - }); - }); - - describe('Pagination', () => { - beforeEach(() => { - wrapper = createComponent(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - wrapper.destroy(); - }); - - it('will display the pagination component', () => { - expect(findPagination().exists()).toBe(true); - }); - - it('clicking prev or next will emit an event', async () => { - expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); - - findPagination().vm.$emit('input', 2); - await nextTick(); - - expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([{ page: 2 }]); - }); - - it('clicking prev or next will send tracking information', () => { - findPagination().vm.$emit('input', 2); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { label: 'pagination' }); - }); - - describe('with `hasNextPage=false', () => { - beforeEach(() => { - wrapper = createComponent({ pagination: { page: 1, hasNextPage: false } }); - }); - - it('will not display the pagination component', () => { - expect(findPagination().exists()).toBe(false); - }); - }); - }); - - describe('Sorting', () => { - const triggerTableSort = (sortDesc = true) => - findTable().vm.$emit('sort-changed', { - sortBy: PAGINATION_SORT_FIELD_DURATION, - sortDesc, - }); - - beforeEach(() => { - wrapper = createComponent(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - wrapper.destroy(); - }); - - it('can sort the end event or duration', () => { - findTableHeadColumns() - .wrappers.slice(1) - .forEach((w) => { - expect(w.attributes('aria-sort')).toBe('none'); - }); - }); - - it('cannot be sorted by title', () => { - findTableHeadColumns() - .wrappers.slice(0, 1) - .forEach((w) => { - expect(w.attributes('aria-sort')).toBeUndefined(); - }); - }); - - it('clicking a table column will send tracking information', () => { - triggerTableSort(); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'sort_duration_desc', - }); - }); - - it('clicking a table column will update the sort field', () => { - expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); - triggerTableSort(); - - expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ - { - direction: 'desc', - sort: 'duration', - }, - ]); - }); - - it('with sortDesc=false will toggle the direction field', () => { - expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); - triggerTableSort(false); - - expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ - { - direction: 'asc', - sort: 'duration', - }, - ]); - }); - - describe('with sortable=false', () => { - beforeEach(() => { - wrapper = createComponent({ sortable: false }); - }); - - it('cannot sort the table', () => { - findTableHeadColumns().wrappers.forEach((w) => { - expect(w.attributes('aria-sort')).toBeUndefined(); - }); - }); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js deleted file mode 100644 index 94b6de85a5c..00000000000 --- a/spec/frontend/cycle_analytics/store/actions_spec.js +++ /dev/null @@ -1,518 +0,0 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/cycle_analytics/store/actions'; -import * as getters from '~/cycle_analytics/store/getters'; -import httpStatusCodes from '~/lib/utils/http_status'; -import { - allowedStages, - selectedStage, - selectedValueStream, - currentGroup, - createdAfter, - createdBefore, - initialPaginationState, - reviewEvents, -} from '../mock_data'; - -const { id: groupId, path: groupPath } = currentGroup; -const mockMilestonesPath = 'mock-milestones.json'; -const mockLabelsPath = 'mock-labels.json'; -const mockRequestPath = 'some/cool/path'; -const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams'; -const mockEndpoints = { - fullPath: mockFullPath, - requestPath: mockRequestPath, - labelsPath: mockLabelsPath, - milestonesPath: mockMilestonesPath, - groupId, - groupPath, -}; -const mockSetDateActionCommit = { - payload: { createdAfter, createdBefore }, - type: 'SET_DATE_RANGE', -}; - -const defaultState = { - ...getters, - selectedValueStream, - createdAfter, - createdBefore, - pagination: initialPaginationState, -}; - -describe('Project Value Stream Analytics actions', () => { - let state; - let mock; - - beforeEach(() => { - state = { ...defaultState }; - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - state = {}; - }); - - const mutationTypes = (arr) => arr.map(({ type }) => type); - - describe.each` - action | payload | expectedActions | expectedMutations - ${'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}`, () => - testAction({ - action: actions[action], - state, - payload, - expectedMutations, - expectedActions, - })); - }); - - describe('initializeVsa', () => { - 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, - milestonesEndpoint: mockMilestonesPath, - 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(() => { - state = { ...state, stages: allowedStages }; - }); - - 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: [ - { - type: 'SET_SELECTED_STAGE', - payload: fakeStage, - }, - ], - expectedActions: [{ type: 'fetchValueStreamStageData' }], - }); - }); - }); - - 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' }], - }); - }); - }); - - describe('with no value stream stages available', () => { - it('will return SET_NO_ACCESS_ERROR', () => { - state = { ...state, stages: [] }; - testAction({ - action: actions.setInitialStage, - state, - payload: null, - expectedMutations: [{ type: 'SET_NO_ACCESS_ERROR' }], - expectedActions: [], - }); - }); - }); - }); - - describe('updateStageTablePagination', () => { - beforeEach(() => { - state = { ...state, selectedStage }; - }); - - it(`will dispatch the "fetchStageData" action and commit the 'SET_PAGINATION' mutation`, () => { - return testAction({ - action: actions.updateStageTablePagination, - state, - expectedMutations: [{ type: 'SET_PAGINATION' }], - expectedActions: [{ type: 'fetchStageData', payload: selectedStage.id }], - }); - }); - }); - - describe('fetchStageData', () => { - const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/; - const headers = { - 'X-Next-Page': 2, - 'X-Page': 1, - }; - - beforeEach(() => { - state = { - ...defaultState, - endpoints: mockEndpoints, - selectedStage, - }; - mock = new MockAdapter(axios); - mock.onGet(mockStagePath).reply(httpStatusCodes.OK, reviewEvents, headers); - }); - - it(`commits the 'RECEIVE_STAGE_DATA_SUCCESS' mutation`, () => - testAction({ - action: actions.fetchStageData, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_STAGE_DATA' }, - { type: 'RECEIVE_STAGE_DATA_SUCCESS', payload: reviewEvents }, - { type: 'SET_PAGINATION', payload: { hasNextPage: true, page: 1 } }, - ], - expectedActions: [], - })); - - describe('with a successful request, but an error in the payload', () => { - const tooMuchDataError = 'Too much data'; - - beforeEach(() => { - state = { - ...defaultState, - endpoints: mockEndpoints, - selectedStage, - }; - mock = new MockAdapter(axios); - mock.onGet(mockStagePath).reply(httpStatusCodes.OK, { error: tooMuchDataError }); - }); - - it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () => - testAction({ - action: actions.fetchStageData, - state, - payload: { error: tooMuchDataError }, - expectedMutations: [ - { type: 'REQUEST_STAGE_DATA' }, - { type: 'RECEIVE_STAGE_DATA_ERROR', payload: tooMuchDataError }, - ], - expectedActions: [], - })); - }); - - describe('with a failing request', () => { - beforeEach(() => { - state = { - ...defaultState, - endpoints: mockEndpoints, - selectedStage, - }; - mock = new MockAdapter(axios); - mock.onGet(mockStagePath).reply(httpStatusCodes.BAD_REQUEST); - }); - - it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () => - testAction({ - action: actions.fetchStageData, - state, - payload: {}, - expectedMutations: [{ type: 'REQUEST_STAGE_DATA' }, { type: 'RECEIVE_STAGE_DATA_ERROR' }], - expectedActions: [], - })); - }); - }); - - describe('fetchValueStreams', () => { - const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/; - - beforeEach(() => { - state = { - endpoints: mockEndpoints, - }; - mock = new MockAdapter(axios); - mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK); - }); - - it(`commits the 'REQUEST_VALUE_STREAMS' mutation`, () => - testAction({ - action: actions.fetchValueStreams, - state, - payload: {}, - expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }], - expectedActions: [{ type: 'receiveValueStreamsSuccess' }], - })); - - describe('with a failing request', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST); - }); - - it(`commits the 'RECEIVE_VALUE_STREAMS_ERROR' mutation`, () => - testAction({ - action: actions.fetchValueStreams, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_VALUE_STREAMS' }, - { type: 'RECEIVE_VALUE_STREAMS_ERROR', payload: httpStatusCodes.BAD_REQUEST }, - ], - expectedActions: [], - })); - }); - }); - - describe('receiveValueStreamsSuccess', () => { - const mockValueStream = { - id: 'mockDefault', - name: 'mock default', - }; - const mockValueStreams = [mockValueStream, selectedValueStream]; - it('with data, will set the first value stream', () => { - testAction({ - action: actions.receiveValueStreamsSuccess, - state, - payload: mockValueStreams, - expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: mockValueStreams }], - expectedActions: [{ type: 'setSelectedValueStream', payload: mockValueStream }], - }); - }); - - it('without data, will set the default value stream', () => { - testAction({ - action: actions.receiveValueStreamsSuccess, - state, - payload: [], - expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: [] }], - expectedActions: [{ type: 'setSelectedValueStream', payload: selectedValueStream }], - }); - }); - }); - - describe('fetchValueStreamStages', () => { - const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/; - - beforeEach(() => { - state = { - endpoints: mockEndpoints, - selectedValueStream, - }; - mock = new MockAdapter(axios); - mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK); - }); - - it(`commits the 'REQUEST_VALUE_STREAM_STAGES' and 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' mutations`, () => - testAction({ - action: actions.fetchValueStreamStages, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_VALUE_STREAM_STAGES' }, - { type: 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' }, - ], - 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.fetchValueStreamStages, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_VALUE_STREAM_STAGES' }, - { type: 'RECEIVE_VALUE_STREAM_STAGES_ERROR', payload: httpStatusCodes.BAD_REQUEST }, - ], - expectedActions: [], - })); - }); - }); - - 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: [], - })); - }); - }); - - describe('fetchStageCountValues', () => { - const mockValueStreamPath = /count/; - const stageCountsPayload = [ - { id: 'issue', count: 1 }, - { id: 'plan', count: 2 }, - { id: 'code', count: 3 }, - ]; - - const stageCountError = new Error( - `Request failed with status code ${httpStatusCodes.BAD_REQUEST}`, - ); - - beforeEach(() => { - state = { - fullPath: mockFullPath, - selectedValueStream, - stages: allowedStages, - }; - mock = new MockAdapter(axios); - mock - .onGet(mockValueStreamPath) - .replyOnce(httpStatusCodes.OK, { count: 1 }) - .onGet(mockValueStreamPath) - .replyOnce(httpStatusCodes.OK, { count: 2 }) - .onGet(mockValueStreamPath) - .replyOnce(httpStatusCodes.OK, { count: 3 }); - }); - - it(`commits the 'REQUEST_STAGE_COUNTS' and 'RECEIVE_STAGE_COUNTS_SUCCESS' mutations`, () => - testAction({ - action: actions.fetchStageCountValues, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_STAGE_COUNTS' }, - { type: 'RECEIVE_STAGE_COUNTS_SUCCESS', payload: stageCountsPayload }, - ], - expectedActions: [], - })); - - describe('with a failing request', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST); - }); - - it(`commits the 'RECEIVE_STAGE_COUNTS_ERROR' mutation`, () => - testAction({ - action: actions.fetchStageCountValues, - state, - payload: {}, - expectedMutations: [ - { type: 'REQUEST_STAGE_COUNTS' }, - { type: 'RECEIVE_STAGE_COUNTS_ERROR', payload: stageCountError }, - ], - expectedActions: [], - })); - }); - }); - - 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 fetchStageData, fetchStageMedians and fetchStageCountValues actions', () => - testAction({ - action: actions.fetchValueStreamStageData, - state, - payload: {}, - expectedMutations: [], - expectedActions: [ - { type: 'fetchStageData' }, - { type: 'fetchStageMedians' }, - { type: 'fetchStageCountValues' }, - ], - })); - }); -}); diff --git a/spec/frontend/cycle_analytics/store/getters_spec.js b/spec/frontend/cycle_analytics/store/getters_spec.js deleted file mode 100644 index c9208045a68..00000000000 --- a/spec/frontend/cycle_analytics/store/getters_spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import * as getters from '~/cycle_analytics/store/getters'; - -import { - allowedStages, - stageMedians, - transformedProjectStagePathData, - selectedStage, - stageCounts, - basePaginationResult, - initialPaginationState, -} from '../mock_data'; - -describe('Value stream analytics getters', () => { - let state = {}; - - describe('pathNavigationData', () => { - it('returns the transformed data', () => { - state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts }; - expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData); - }); - }); - - describe('paginationParams', () => { - beforeEach(() => { - state = { pagination: initialPaginationState }; - }); - - it('returns the `pagination` type', () => { - expect(getters.paginationParams(state)).toEqual(basePaginationResult); - }); - - it('returns the `sort` type', () => { - expect(getters.paginationParams(state)).toEqual(basePaginationResult); - }); - - it('with page=10, sets the `page` property', () => { - const page = 10; - state = { pagination: { ...initialPaginationState, page } }; - expect(getters.paginationParams(state)).toEqual({ ...basePaginationResult, page }); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js deleted file mode 100644 index 2e9e5d91471..00000000000 --- a/spec/frontend/cycle_analytics/store/mutations_spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import { useFakeDate } from 'helpers/fake_date'; -import * as types from '~/cycle_analytics/store/mutation_types'; -import mutations from '~/cycle_analytics/store/mutations'; -import { - PAGINATION_SORT_FIELD_END_EVENT, - PAGINATION_SORT_DIRECTION_DESC, -} from '~/cycle_analytics/constants'; -import { - selectedStage, - rawIssueEvents, - issueEvents, - selectedValueStream, - rawValueStreamStages, - valueStreamStages, - rawStageMedians, - formattedStageMedians, - rawStageCounts, - stageCounts, - initialPaginationState as pagination, -} from '../mock_data'; - -let state; -const rawEvents = rawIssueEvents.events; -const convertedEvents = issueEvents.events; -const mockRequestPath = 'fake/request/path'; -const mockCreatedAfter = '2020-06-18'; -const mockCreatedBefore = '2020-07-18'; - -describe('Project Value Stream Analytics mutations', () => { - useFakeDate(2020, 6, 18); - - beforeEach(() => { - state = { pagination }; - }); - - afterEach(() => { - state = null; - }); - - it.each` - mutation | stateKey | value - ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]} - ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]} - ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]} - ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]} - ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} - ${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false} - ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'isLoadingStage'} | ${false} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} - ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} - ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} - ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}} - ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}} - ${types.SET_NO_ACCESS_ERROR} | ${'hasNoAccessError'} | ${true} - `('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => { - mutations[mutation](state); - - expect(state).toMatchObject({ [stateKey]: value }); - }); - - const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore }; - const mockInitialPayload = { - endpoints: { requestPath: mockRequestPath }, - currentGroup: { title: 'cool-group' }, - id: 1337, - ...mockSetDatePayload, - }; - const mockInitializedObj = { - endpoints: { requestPath: mockRequestPath }, - ...mockSetDatePayload, - }; - - it.each` - mutation | stateKey | value - ${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }} - ${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter} - ${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore} - `('$mutation will set $stateKey', ({ mutation, stateKey, value }) => { - mutations[mutation](state, { ...mockInitialPayload }); - - expect(state).toMatchObject({ ...mockInitializedObj, [stateKey]: value }); - }); - - it.each` - mutation | payload | stateKey | value - ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter} - ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore} - ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} - ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false} - ${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} - ${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts} - `( - '$mutation with $payload will set $stateKey to $value', - ({ mutation, payload, stateKey, value }) => { - mutations[mutation](state, payload); - - expect(state).toMatchObject({ [stateKey]: value }); - }, - ); - - describe('with a stage selected', () => { - beforeEach(() => { - state = { - selectedStage, - }; - }); - - it.each` - mutation | payload | stateKey | value - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${[]} | ${'isEmptyStage'} | ${true} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${rawEvents} | ${'selectedStageEvents'} | ${convertedEvents} - ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${rawEvents} | ${'isEmptyStage'} | ${false} - `( - '$mutation with $payload will set $stateKey to $value', - ({ mutation, payload, stateKey, value }) => { - mutations[mutation](state, payload); - - expect(state).toMatchObject({ [stateKey]: value }); - }, - ); - }); -}); diff --git a/spec/frontend/cycle_analytics/total_time_spec.js b/spec/frontend/cycle_analytics/total_time_spec.js deleted file mode 100644 index 8cf9feab6e9..00000000000 --- a/spec/frontend/cycle_analytics/total_time_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import { mount } from '@vue/test-utils'; -import TotalTime from '~/cycle_analytics/components/total_time.vue'; - -describe('TotalTime', () => { - let wrapper = null; - - const createComponent = (propsData) => { - return mount(TotalTime, { - propsData, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('with a valid time object', () => { - it.each` - time - ${{ seconds: 35 }} - ${{ mins: 47, seconds: 3 }} - ${{ days: 3, mins: 47, seconds: 3 }} - ${{ hours: 23, mins: 10 }} - ${{ hours: 7, mins: 20, seconds: 10 }} - `('with $time', ({ time }) => { - wrapper = createComponent({ - time, - }); - - expect(wrapper.html()).toMatchSnapshot(); - }); - }); - - describe('with a blank object', () => { - beforeEach(() => { - wrapper = createComponent({ - time: {}, - }); - }); - - it('should render --', () => { - expect(wrapper.html()).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js deleted file mode 100644 index 51405a1ba4d..00000000000 --- a/spec/frontend/cycle_analytics/utils_spec.js +++ /dev/null @@ -1,171 +0,0 @@ -import { - transformStagesForPathNavigation, - medianTimeToParsedSeconds, - formatMedianValues, - filterStagesByHiddenStatus, - buildCycleAnalyticsInitialData, -} from '~/cycle_analytics/utils'; -import { - selectedStage, - allowedStages, - stageMedians, - pathNavIssueMetric, - rawStageMedians, -} from './mock_data'; - -describe('Value stream analytics utils', () => { - describe('transformStagesForPathNavigation', () => { - const stages = allowedStages; - const response = transformStagesForPathNavigation({ - stages, - medians: stageMedians, - selectedStage, - }); - - describe('transforms the data as expected', () => { - it('returns an array of stages', () => { - expect(Array.isArray(response)).toBe(true); - expect(response.length).toBe(stages.length); - }); - - it('selects the correct stage', () => { - const selected = response.filter((stage) => stage.selected === true)[0]; - - expect(selected.title).toBe(selectedStage.title); - }); - - it('includes the correct metric for the associated stage', () => { - const issue = response.filter((stage) => stage.name === 'issue')[0]; - - expect(issue.metric).toBe(pathNavIssueMetric); - }); - }); - }); - - describe('medianTimeToParsedSeconds', () => { - it.each` - value | result - ${1036800} | ${'1w'} - ${259200} | ${'3d'} - ${172800} | ${'2d'} - ${86400} | ${'1d'} - ${1000} | ${'16m'} - ${61} | ${'1m'} - ${59} | ${'<1m'} - ${0} | ${'-'} - `('will correctly parse $value seconds into $result', ({ value, result }) => { - expect(medianTimeToParsedSeconds(value)).toBe(result); - }); - }); - - describe('formatMedianValues', () => { - const calculatedMedians = formatMedianValues(rawStageMedians); - - it('returns an object with each stage and their median formatted for display', () => { - rawStageMedians.forEach(({ id, value }) => { - expect(calculatedMedians).toMatchObject({ [id]: medianTimeToParsedSeconds(value) }); - }); - }); - }); - - describe('filterStagesByHiddenStatus', () => { - const hiddenStages = [{ title: 'three', hidden: true }]; - const visibleStages = [ - { title: 'one', hidden: false }, - { title: 'two', hidden: false }, - ]; - const mockStages = [...visibleStages, ...hiddenStages]; - - it.each` - isHidden | result - ${false} | ${visibleStages} - ${undefined} | ${hiddenStages} - ${true} | ${hiddenStages} - `('with isHidden=$isHidden returns matching stages', ({ isHidden, result }) => { - expect(filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result); - }); - }); - - 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); - }); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/cycle_analytics/value_stream_filters_spec.js deleted file mode 100644 index 6e96a6d756a..00000000000 --- a/spec/frontend/cycle_analytics/value_stream_filters_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Daterange from '~/analytics/shared/components/daterange.vue'; -import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; -import FilterBar from '~/cycle_analytics/components/filter_bar.vue'; -import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue'; -import { - createdAfter as startDate, - createdBefore as endDate, - currentGroup, - selectedProjects, -} from './mock_data'; - -function createComponent(props = {}) { - return shallowMount(ValueStreamFilters, { - propsData: { - selectedProjects, - groupId: currentGroup.id, - groupPath: currentGroup.fullPath, - startDate, - endDate, - ...props, - }, - }); -} - -describe('ValueStreamFilters', () => { - let wrapper; - - const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdownFilter); - const findDateRangePicker = () => wrapper.findComponent(Daterange); - const findFilterBar = () => wrapper.findComponent(FilterBar); - - beforeEach(() => { - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('will render the filter bar', () => { - expect(findFilterBar().exists()).toBe(true); - }); - - it('will render the projects dropdown', () => { - expect(findProjectsDropdown().exists()).toBe(true); - expect(wrapper.findComponent(ProjectsDropdownFilter).props()).toEqual( - expect.objectContaining({ - queryParams: wrapper.vm.projectsQueryParams, - multiSelect: wrapper.vm.$options.multiProjectSelect, - }), - ); - }); - - it('will render the date range picker', () => { - expect(findDateRangePicker().exists()).toBe(true); - }); - - it('will emit `selectProject` when a project is selected', () => { - findProjectsDropdown().vm.$emit('selected'); - - expect(wrapper.emitted('selectProject')).not.toBeUndefined(); - }); - - it('will emit `setDateRange` when the date range changes', () => { - findDateRangePicker().vm.$emit('change'); - - expect(wrapper.emitted('setDateRange')).not.toBeUndefined(); - }); - - describe('hasDateRangeFilter = false', () => { - beforeEach(() => { - wrapper = createComponent({ hasDateRangeFilter: false }); - }); - - it('will not render the date range picker', () => { - expect(findDateRangePicker().exists()).toBe(false); - }); - }); - - describe('hasProjectFilter = false', () => { - beforeEach(() => { - wrapper = createComponent({ hasProjectFilter: false }); - }); - - it('will not render the project dropdown', () => { - expect(findProjectsDropdown().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js deleted file mode 100644 index 948dc5c9be2..00000000000 --- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js +++ /dev/null @@ -1,185 +0,0 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; -import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api'; -import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants'; -import { prepareTimeMetricsData } from '~/analytics/shared/utils'; -import MetricTile from '~/analytics/shared/components/metric_tile.vue'; -import { createAlert } from '~/flash'; -import { group } from './mock_data'; - -jest.mock('~/flash'); - -describe('ValueStreamMetrics', () => { - let wrapper; - let mockGetValueStreamSummaryMetrics; - let mockFilterFn; - - const { full_path: requestPath } = group; - const fakeReqName = 'Mock metrics'; - const metricsRequestFactory = () => ({ - request: mockGetValueStreamSummaryMetrics, - endpoint: METRIC_TYPE_SUMMARY, - name: fakeReqName, - }); - - const createComponent = (props = {}) => { - return shallowMountExtended(ValueStreamMetrics, { - propsData: { - requestPath, - requestParams: {}, - requests: [metricsRequestFactory()], - ...props, - }, - }); - }; - - const findMetrics = () => wrapper.findAllComponents(MetricTile); - const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group'); - - const expectToHaveRequest = (fields) => { - expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({ - endpoint: METRIC_TYPE_SUMMARY, - requestPath, - ...fields, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('with successful requests', () => { - beforeEach(() => { - mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData }); - }); - - it('will display a loader with pending requests', async () => { - wrapper = createComponent(); - await nextTick(); - - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); - }); - - describe('with data loaded', () => { - beforeEach(async () => { - wrapper = createComponent(); - await waitForPromises(); - }); - - it('fetches data from the value stream analytics endpoint', () => { - expectToHaveRequest({ params: {} }); - }); - - describe.each` - index | identifier | value | label - ${0} | ${metricsData[0].identifier} | ${metricsData[0].value} | ${metricsData[0].title} - ${1} | ${metricsData[1].identifier} | ${metricsData[1].value} | ${metricsData[1].title} - ${2} | ${metricsData[2].identifier} | ${metricsData[2].value} | ${metricsData[2].title} - ${3} | ${metricsData[3].identifier} | ${metricsData[3].value} | ${metricsData[3].title} - `('metric tiles', ({ identifier, index, value, label }) => { - it(`renders a metric tile component for "${label}"`, () => { - const metric = findMetrics().at(index); - expect(metric.props('metric')).toMatchObject({ identifier, value, label }); - expect(metric.isVisible()).toBe(true); - }); - }); - - it('will not display a loading icon', () => { - expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); - }); - - describe('filterFn', () => { - const transferredMetricsData = prepareTimeMetricsData(metricsData, METRICS_POPOVER_CONTENT); - - it('with a filter function, will call the function with the metrics data', async () => { - const filteredData = [ - { identifier: 'issues', value: '3', title: 'New Issues', description: 'foo' }, - ]; - mockFilterFn = jest.fn(() => filteredData); - - wrapper = createComponent({ - filterFn: mockFilterFn, - }); - - await waitForPromises(); - - expect(mockFilterFn).toHaveBeenCalledWith(transferredMetricsData); - expect(wrapper.vm.metrics).toEqual(filteredData); - }); - - it('without a filter function, it will only update the metrics', async () => { - wrapper = createComponent(); - - await waitForPromises(); - - expect(mockFilterFn).not.toHaveBeenCalled(); - expect(wrapper.vm.metrics).toEqual(transferredMetricsData); - }); - }); - - describe('with additional params', () => { - beforeEach(async () => { - wrapper = createComponent({ - requestParams: { - 'project_ids[]': [1], - created_after: '2020-01-01', - created_before: '2020-02-01', - }, - }); - - await waitForPromises(); - }); - - it('fetches data for the `getValueStreamSummaryMetrics` request', () => { - expectToHaveRequest({ - params: { - 'project_ids[]': [1], - created_after: '2020-01-01', - created_before: '2020-02-01', - }, - }); - }); - }); - - describe('groupBy', () => { - beforeEach(async () => { - mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData }); - wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS }); - await waitForPromises(); - }); - - it('renders the metrics as separate groups', () => { - const groups = findMetricsGroups(); - expect(groups).toHaveLength(VSA_METRICS_GROUPS.length); - }); - - it('renders titles for each group', () => { - const groups = findMetricsGroups(); - groups.wrappers.forEach((g, index) => { - const { title } = VSA_METRICS_GROUPS[index]; - expect(g.html()).toContain(title); - }); - }); - }); - }); - }); - - describe('with a request failing', () => { - beforeEach(async () => { - mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue(); - wrapper = createComponent(); - - await waitForPromises(); - }); - - it('should render an error message', () => { - expect(createAlert).toHaveBeenCalledWith({ - message: `There was an error while fetching value stream analytics ${fakeReqName} data.`, - }); - }); - }); -}); |