diff options
Diffstat (limited to 'spec/frontend/analytics')
9 files changed, 279 insertions, 63 deletions
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js index 854582abb82..a38df274243 100644 --- a/spec/frontend/analytics/shared/components/daterange_spec.js +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -1,7 +1,6 @@ -import { GlDaterangePicker } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlDaterangePicker, GlSprintf } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; import { useFakeDate } from 'helpers/fake_date'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import Daterange from '~/analytics/shared/components/daterange.vue'; const defaultProps = { @@ -14,13 +13,13 @@ describe('Daterange component', () => { let wrapper; - const factory = (props = defaultProps) => { - wrapper = mount(Daterange, { + const factory = (props = defaultProps, mountFn = shallowMount) => { + wrapper = mountFn(Daterange, { propsData: { ...defaultProps, ...props, }, - directives: { GlTooltip: createMockDirective() }, + stubs: { GlSprintf }, }); }; @@ -28,9 +27,8 @@ describe('Daterange component', () => { wrapper.destroy(); }); - const findDaterangePicker = () => wrapper.find(GlDaterangePicker); - - const findDateRangeIndicator = () => wrapper.find('.daterange-indicator'); + const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker); + const findDateRangeIndicator = () => wrapper.findComponent(GlSprintf); describe('template', () => { describe('when show is false', () => { @@ -43,26 +41,24 @@ describe('Daterange component', () => { describe('when show is true', () => { it('renders the daterange picker', () => { factory({ show: true }); + expect(findDaterangePicker().exists()).toBe(true); }); }); describe('with a minDate being set', () => { - it('emits the change event with the minDate when the user enters a start date before the minDate', () => { + it('emits the change event with the minDate when the user enters a start date before the minDate', async () => { const startDate = new Date('2019-09-01'); const endDate = new Date('2019-09-30'); const minDate = new Date('2019-06-01'); - factory({ show: true, startDate, endDate, minDate }); - + factory({ show: true, startDate, endDate, minDate }, mount); const input = findDaterangePicker().find('input'); input.setValue('2019-01-01'); - input.trigger('change'); + await input.trigger('change'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]); - }); + expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]); }); }); @@ -76,16 +72,13 @@ describe('Daterange component', () => { }); it('displays the correct number of selected days in the indicator', () => { - expect(findDateRangeIndicator().find('span').text()).toBe('10 days selected'); + expect(findDateRangeIndicator().text()).toMatchInterpolatedText('10 days selected'); }); - it('displays a tooltip', () => { - const icon = wrapper.find('[data-testid="helper-icon"]'); - const tooltip = getBinding(icon.element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - expect(icon.attributes('title')).toBe( - 'Showing data for workflow items created in this date range. Date range cannot exceed 30 days.', + it('sets the tooltip', () => { + const tooltip = findDaterangePicker().props('tooltip'); + expect(tooltip).toBe( + 'Showing data for workflow items created in this date range. Date range limited to 30 days.', ); }); }); diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js new file mode 100644 index 00000000000..b799c911488 --- /dev/null +++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js @@ -0,0 +1,102 @@ +import { GlLink, GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import MetricPopover from '~/analytics/shared/components/metric_popover.vue'; + +const MOCK_METRIC = { + key: 'deployment-frequency', + label: 'Deployment Frequency', + value: '10.0', + unit: 'per day', + description: 'Average number of deployments to production per day.', + links: [], +}; + +describe('MetricPopover', () => { + let wrapper; + + const createComponent = (props = {}) => { + return shallowMountExtended(MetricPopover, { + propsData: { + target: 'deployment-frequency', + ...props, + }, + stubs: { + 'gl-popover': { template: '<div><slot name="title"></slot><slot></slot></div>' }, + }, + }); + }; + + const findMetricLabel = () => wrapper.findByTestId('metric-label'); + const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]'); + const findMetricDescription = () => wrapper.findByTestId('metric-description'); + const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link'); + const findMetricDocsLinkIcon = () => findMetricDocsLink().find(GlIcon); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the metric label', () => { + wrapper = createComponent({ metric: MOCK_METRIC }); + expect(findMetricLabel().text()).toBe(MOCK_METRIC.label); + }); + + it('renders the metric description', () => { + wrapper = createComponent({ metric: MOCK_METRIC }); + expect(findMetricDescription().text()).toBe(MOCK_METRIC.description); + }); + + describe('with links', () => { + const links = [ + { + name: 'Deployment frequency', + url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency', + label: 'Dashboard', + }, + { + name: 'Another link', + url: '/groups/gitlab-org/-/analytics/another-link', + label: 'Another link', + }, + ]; + const docsLink = { + name: 'Deployment frequency', + url: '/help/user/analytics/index#definitions', + label: 'Go to docs', + docs_link: true, + }; + const linksWithDocs = [...links, docsLink]; + + describe.each` + hasDocsLink | allLinks | displayedMetricLinks + ${true} | ${linksWithDocs} | ${links} + ${false} | ${links} | ${links} + `( + 'when one link has docs_link=$hasDocsLink', + ({ hasDocsLink, allLinks, displayedMetricLinks }) => { + beforeEach(() => { + wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } }); + }); + + displayedMetricLinks.forEach((link, idx) => { + it(`renders a link for "${link.name}"`, () => { + const allLinkContainers = findAllMetricLinks(); + + expect(allLinkContainers.at(idx).text()).toContain(link.name); + expect(allLinkContainers.at(idx).find(GlLink).attributes('href')).toBe(link.url); + }); + }); + + it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => { + expect(findMetricDocsLink().exists()).toBe(hasDocsLink); + + if (hasDocsLink) { + expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url); + expect(findMetricDocsLink().text()).toBe(docsLink.label); + expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link'); + } + }); + }, + ); + }); +}); diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js new file mode 100644 index 00000000000..980dfad9eb0 --- /dev/null +++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js @@ -0,0 +1,81 @@ +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { shallowMount } from '@vue/test-utils'; +import MetricTile from '~/analytics/shared/components/metric_tile.vue'; +import MetricPopover from '~/analytics/shared/components/metric_popover.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility'); + +describe('MetricTile', () => { + let wrapper; + + const createComponent = (props = {}) => { + return shallowMount(MetricTile, { + propsData: { + metric: {}, + ...props, + }, + }); + }; + + const findSingleStat = () => wrapper.findComponent(GlSingleStat); + const findPopover = () => wrapper.findComponent(MetricPopover); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + describe('links', () => { + it('when the metric has links, it redirects the user on click', () => { + const metric = { + identifier: 'deploys', + value: '10', + label: 'Deploys', + links: [{ url: 'foo/bar' }], + }; + wrapper = createComponent({ metric }); + + const singleStat = findSingleStat(); + singleStat.vm.$emit('click'); + expect(redirectTo).toHaveBeenCalledWith('foo/bar'); + }); + + it("when the metric doesn't have links, it won't the user on click", () => { + const metric = { identifier: 'deploys', value: '10', label: 'Deploys' }; + wrapper = createComponent({ metric }); + + const singleStat = findSingleStat(); + singleStat.vm.$emit('click'); + expect(redirectTo).not.toHaveBeenCalled(); + }); + }); + + describe('decimal places', () => { + it(`will render 0 decimal places for an integer value`, () => { + const metric = { identifier: 'deploys', value: '10', label: 'Deploys' }; + wrapper = createComponent({ metric }); + + const singleStat = findSingleStat(); + expect(singleStat.props('animationDecimalPlaces')).toBe(0); + }); + + it(`will render 1 decimal place for a float value`, () => { + const metric = { identifier: 'deploys', value: '10.5', label: 'Deploys' }; + wrapper = createComponent({ metric }); + + const singleStat = findSingleStat(); + expect(singleStat.props('animationDecimalPlaces')).toBe(1); + }); + }); + + it('renders a metric popover', () => { + const metric = { identifier: 'deploys', value: '10', label: 'Deploys' }; + wrapper = createComponent({ metric }); + + const popover = findPopover(); + expect(popover.exists()).toBe(true); + expect(popover.props()).toMatchObject({ metric, target: metric.identifier }); + }); + }); +}); diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js index 28d7ebe28df..386fb4eb616 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -1,4 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'helpers/test_constants'; @@ -99,9 +100,9 @@ describe('ProjectsDropdownFilter component', () => { const findDropdownFullPathAtIndex = (index) => findDropdownAtIndex(index).find('[data-testid="project-full-path"]'); - const selectDropdownItemAtIndex = (index) => { + const selectDropdownItemAtIndex = async (index) => { findDropdownAtIndex(index).find('button').trigger('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }; // NOTE: Selected items are now visually separated from unselected items @@ -132,16 +133,15 @@ describe('ProjectsDropdownFilter component', () => { expect(spyQuery).toHaveBeenCalledTimes(1); - await wrapper.vm.$nextTick(() => { - expect(spyQuery).toHaveBeenCalledWith({ - query: getProjects, - variables: { - search: 'gitlab', - groupFullPath: wrapper.vm.groupNamespace, - first: 50, - includeSubgroups: true, - }, - }); + await nextTick(); + expect(spyQuery).toHaveBeenCalledWith({ + query: getProjects, + variables: { + search: 'gitlab', + groupFullPath: wrapper.vm.groupNamespace, + first: 50, + includeSubgroups: true, + }, }); }); }); @@ -193,7 +193,7 @@ describe('ProjectsDropdownFilter component', () => { expect(wrapper.text()).toContain('2 projects selected'); findClearAllButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.text()).not.toContain('2 projects selected'); expect(wrapper.text()).toContain('Select projects'); @@ -366,9 +366,8 @@ describe('ProjectsDropdownFilter component', () => { selectDropdownItemAtIndex(0); selectDropdownItemAtIndex(1); - await wrapper.vm.$nextTick().then(() => { - expect(findDropdownButton().text()).toBe('2 projects selected'); - }); + await nextTick(); + expect(findDropdownButton().text()).toBe('2 projects selected'); }); }); }); diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js index 0513ccb2890..b48e2d971b5 100644 --- a/spec/frontend/analytics/shared/utils_spec.js +++ b/spec/frontend/analytics/shared/utils_spec.js @@ -1,9 +1,12 @@ +import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json'; import { filterBySearchTerm, extractFilterQueryParameters, extractPaginationQueryParameters, getDataZoomOption, + prepareTimeMetricsData, } from '~/analytics/shared/utils'; +import { slugify } from '~/lib/utils/text_utility'; import { objectToQuery } from '~/lib/utils/url_utility'; describe('filterBySearchTerm', () => { @@ -176,3 +179,36 @@ describe('getDataZoomOption', () => { }); }); }); + +describe('prepareTimeMetricsData', () => { + let prepared; + const [first, second] = metricsData; + delete second.identifier; // testing the case when identifier is missing + + const firstIdentifier = first.identifier; + const secondIdentifier = slugify(second.title); + + beforeEach(() => { + prepared = prepareTimeMetricsData([first, second], { + [firstIdentifier]: { description: 'Is a value that is good' }, + }); + }); + + it('will add a `identifier` based on the title', () => { + expect(prepared).toMatchObject([ + { identifier: firstIdentifier }, + { identifier: secondIdentifier }, + ]); + }); + + it('will add a `label` key', () => { + expect(prepared).toMatchObject([{ label: 'New Issues' }, { label: 'Commits' }]); + }); + + it('will add a popover description using the key if it is provided', () => { + expect(prepared).toMatchObject([ + { description: 'Is a value that is good' }, + { description: '' }, + ]); + }); +}); diff --git a/spec/frontend/analytics/usage_trends/apollo_mock_data.js b/spec/frontend/analytics/usage_trends/apollo_mock_data.js index 98eabd577ee..934bbc63689 100644 --- a/spec/frontend/analytics/usage_trends/apollo_mock_data.js +++ b/spec/frontend/analytics/usage_trends/apollo_mock_data.js @@ -1,4 +1,5 @@ const defaultPageInfo = { + __typename: 'PageInfo', hasNextPage: false, hasPreviousPage: false, startCursor: null, diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js index 1a331100bb8..02cf7f42a0b 100644 --- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js @@ -1,9 +1,10 @@ import { GlAlert } from '@gitlab/ui'; import { GlLineChart } from '@gitlab/ui/dist/charts'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import UsageTrendsCountChart from '~/analytics/usage_trends/components/usage_trends_count_chart.vue'; import statsQuery from '~/analytics/usage_trends/graphql/queries/usage_count.query.graphql'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; @@ -77,9 +78,10 @@ describe('UsageTrendsCountChart', () => { }); describe('without data', () => { - beforeEach(() => { + beforeEach(async () => { queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: [] }); wrapper = createComponent({ responseHandler: queryHandler }); + await waitForPromises(); }); it('renders an no data message', () => { @@ -96,9 +98,10 @@ describe('UsageTrendsCountChart', () => { }); describe('with data', () => { - beforeEach(() => { + beforeEach(async () => { queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: mockCountsData1 }); wrapper = createComponent({ responseHandler: queryHandler }); + await waitForPromises(); }); it('requests data', () => { @@ -126,7 +129,7 @@ describe('UsageTrendsCountChart', () => { const recordedAt = '2020-08-01'; describe('when the fetchMore query returns data', () => { beforeEach(async () => { - const newData = [{ recordedAt, count: 5 }]; + const newData = [{ __typename: 'UsageTrendsMeasurement', recordedAt, count: 5 }]; queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: mockCountsData1, @@ -134,7 +137,7 @@ describe('UsageTrendsCountChart', () => { }); wrapper = createComponent({ responseHandler: queryHandler }); - await wrapper.vm.$nextTick(); + await waitForPromises(); }); it('requests data twice', () => { @@ -161,7 +164,7 @@ describe('UsageTrendsCountChart', () => { .spyOn(wrapper.vm.$apollo.queries[identifier], 'fetchMore') .mockImplementation(jest.fn().mockRejectedValue()); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('calls fetchMore', () => { diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js index 04ea25a02d5..32a664a5026 100644 --- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js @@ -1,9 +1,10 @@ import { GlAlert } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import UsersChart from '~/analytics/usage_trends/components/users_chart.vue'; import usersQuery from '~/analytics/usage_trends/graphql/queries/users.query.graphql'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; @@ -67,7 +68,7 @@ describe('UsersChart', () => { describe('without data', () => { beforeEach(async () => { wrapper = createComponent({ users: [] }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders an no data message', () => { @@ -86,7 +87,7 @@ describe('UsersChart', () => { describe('with data', () => { beforeEach(async () => { wrapper = createComponent({ users: mockCountsData2 }); - await wrapper.vm.$nextTick(); + await waitForPromises(); }); it('hides the skeleton loader', () => { @@ -107,7 +108,7 @@ describe('UsersChart', () => { describe('with errors', () => { beforeEach(async () => { wrapper = createComponent({ loadingError: true }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders an error message', () => { @@ -134,7 +135,7 @@ describe('UsersChart', () => { }); jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('requests data twice', () => { @@ -147,7 +148,7 @@ describe('UsersChart', () => { }); describe('when the fetchMore query throws an error', () => { - beforeEach(() => { + beforeEach(async () => { wrapper = createComponent({ users: mockCountsData2, additionalData: mockCountsData1, @@ -156,7 +157,7 @@ describe('UsersChart', () => { jest .spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore') .mockImplementation(jest.fn().mockRejectedValue()); - return wrapper.vm.$nextTick(); + await waitForPromises(); }); it('calls fetchMore', () => { diff --git a/spec/frontend/analytics/usage_trends/mock_data.js b/spec/frontend/analytics/usage_trends/mock_data.js index d96dfa26209..77bd44d17f5 100644 --- a/spec/frontend/analytics/usage_trends/mock_data.js +++ b/spec/frontend/analytics/usage_trends/mock_data.js @@ -4,11 +4,11 @@ export const mockUsageCounts = [ ]; export const mockCountsData1 = [ - { recordedAt: '2020-07-23', count: 52 }, - { recordedAt: '2020-07-22', count: 40 }, - { recordedAt: '2020-07-21', count: 31 }, - { recordedAt: '2020-06-14', count: 23 }, - { recordedAt: '2020-06-12', count: 20 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-23', count: 52 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-22', count: 40 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-21', count: 31 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-14', count: 23 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-12', count: 20 }, ]; export const countsMonthlyChartData1 = [ @@ -17,11 +17,11 @@ export const countsMonthlyChartData1 = [ ]; export const mockCountsData2 = [ - { recordedAt: '2020-07-28', count: 10 }, - { recordedAt: '2020-07-27', count: 9 }, - { recordedAt: '2020-06-26', count: 14 }, - { recordedAt: '2020-06-25', count: 23 }, - { recordedAt: '2020-06-24', count: 25 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-28', count: 10 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-27', count: 9 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-26', count: 14 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-25', count: 23 }, + { __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-24', count: 25 }, ]; export const countsMonthlyChartData2 = [ |