diff options
Diffstat (limited to 'spec/frontend/analytics')
10 files changed, 518 insertions, 452 deletions
diff --git a/spec/frontend/analytics/instance_statistics/apollo_mock_data.js b/spec/frontend/analytics/instance_statistics/apollo_mock_data.js index 2e4eaf3fc96..98eabd577ee 100644 --- a/spec/frontend/analytics/instance_statistics/apollo_mock_data.js +++ b/spec/frontend/analytics/instance_statistics/apollo_mock_data.js @@ -1,30 +1,36 @@ -const defaultPageInfo = { hasPreviousPage: false, startCursor: null, endCursor: null }; +const defaultPageInfo = { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, +}; -export function getApolloResponse(options = {}) { - const { - pipelinesTotal = [], - pipelinesSucceeded = [], - pipelinesFailed = [], - pipelinesCanceled = [], - pipelinesSkipped = [], - hasNextPage = false, - } = options; - return { - data: { - pipelinesTotal: { pageInfo: { ...defaultPageInfo, hasNextPage }, nodes: pipelinesTotal }, - pipelinesSucceeded: { - pageInfo: { ...defaultPageInfo, hasNextPage }, - nodes: pipelinesSucceeded, - }, - pipelinesFailed: { pageInfo: { ...defaultPageInfo, hasNextPage }, nodes: pipelinesFailed }, - pipelinesCanceled: { - pageInfo: { ...defaultPageInfo, hasNextPage }, - nodes: pipelinesCanceled, - }, - pipelinesSkipped: { - pageInfo: { ...defaultPageInfo, hasNextPage }, - nodes: pipelinesSkipped, - }, +export const mockApolloResponse = ({ hasNextPage = false, key, data }) => ({ + data: { + [key]: { + pageInfo: { ...defaultPageInfo, hasNextPage }, + nodes: data, }, - }; -} + }, +}); + +export const mockQueryResponse = ({ key, data = [], loading = false, additionalData = [] }) => { + const hasNextPage = Boolean(additionalData.length); + const response = mockApolloResponse({ hasNextPage, key, data }); + if (loading) { + return jest.fn().mockReturnValue(new Promise(() => {})); + } + if (hasNextPage) { + return jest + .fn() + .mockResolvedValueOnce(response) + .mockResolvedValueOnce( + mockApolloResponse({ + hasNextPage: false, + key, + data: additionalData, + }), + ); + } + return jest.fn().mockResolvedValue(response); +}; diff --git a/spec/frontend/analytics/instance_statistics/components/__snapshots__/instance_statistics_count_chart_spec.js.snap b/spec/frontend/analytics/instance_statistics/components/__snapshots__/instance_statistics_count_chart_spec.js.snap new file mode 100644 index 00000000000..29bcd5f223b --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/__snapshots__/instance_statistics_count_chart_spec.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InstanceStatisticsCountChart when fetching more data when the fetchMore query returns data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-07-01", + 41, + ], + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Mock Query", + }, +] +`; + +exports[`InstanceStatisticsCountChart with data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-07-01", + 41, + ], + Array [ + "2020-06-01", + 22, + ], + ], + "name": "Mock Query", + }, +] +`; diff --git a/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap b/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap deleted file mode 100644 index 0b3b685a9f2..00000000000 --- a/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap +++ /dev/null @@ -1,161 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PipelinesChart when fetching more data when the fetchMore query returns data passes the data to the line chart 1`] = ` -Array [ - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - Array [ - "2020-08-01", - 5, - ], - ], - "name": "Total", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - Array [ - "2020-08-01", - 5, - ], - ], - "name": "Succeeded", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 22, - ], - Array [ - "2020-07-01", - 41, - ], - Array [ - "2020-08-01", - 5, - ], - ], - "name": "Failed", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - Array [ - "2020-08-01", - 5, - ], - ], - "name": "Canceled", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - Array [ - "2020-08-01", - 5, - ], - ], - "name": "Skipped", - }, -] -`; - -exports[`PipelinesChart with data passes the data to the line chart 1`] = ` -Array [ - Object { - "data": Array [ - Array [ - "2020-06-01", - 22, - ], - Array [ - "2020-07-01", - 41, - ], - ], - "name": "Total", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - ], - "name": "Succeeded", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 21, - ], - Array [ - "2020-07-01", - 10, - ], - ], - "name": "Failed", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 22, - ], - Array [ - "2020-07-01", - 41, - ], - ], - "name": "Canceled", - }, - Object { - "data": Array [ - Array [ - "2020-06-01", - 22, - ], - Array [ - "2020-07-01", - 41, - ], - ], - "name": "Skipped", - }, -] -`; diff --git a/spec/frontend/analytics/instance_statistics/components/app_spec.js b/spec/frontend/analytics/instance_statistics/components/app_spec.js index df13c9f82a9..8ac663b3046 100644 --- a/spec/frontend/analytics/instance_statistics/components/app_spec.js +++ b/spec/frontend/analytics/instance_statistics/components/app_spec.js @@ -1,8 +1,9 @@ import { shallowMount } from '@vue/test-utils'; import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue'; import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue'; -import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; +import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue'; import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; +import ProjectsAndGroupsChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue'; describe('InstanceStatisticsApp', () => { let wrapper; @@ -24,11 +25,21 @@ describe('InstanceStatisticsApp', () => { expect(wrapper.find(InstanceCounts).exists()).toBe(true); }); - it('displays the pipelines chart component', () => { - expect(wrapper.find(PipelinesChart).exists()).toBe(true); + ['Pipelines', 'Issues & Merge Requests'].forEach(instance => { + it(`displays the ${instance} chart`, () => { + const chartTitles = wrapper + .findAll(InstanceStatisticsCountChart) + .wrappers.map(chartComponent => chartComponent.props('chartTitle')); + + expect(chartTitles).toContain(instance); + }); }); it('displays the users chart component', () => { expect(wrapper.find(UsersChart).exists()).toBe(true); }); + + it('displays the projects and groups chart component', () => { + expect(wrapper.find(ProjectsAndGroupsChart).exists()).toBe(true); + }); }); diff --git a/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js new file mode 100644 index 00000000000..275a84988f8 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js @@ -0,0 +1,177 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue'; +import statsQuery from '~/analytics/instance_statistics/graphql/queries/instance_count.query.graphql'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import { mockCountsData1 } from '../mock_data'; +import { mockQueryResponse, mockApolloResponse } from '../apollo_mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const loadChartErrorMessage = 'My load error message'; +const noDataMessage = 'My no data message'; + +const queryResponseDataKey = 'instanceStatisticsMeasurements'; +const identifier = 'MOCK_QUERY'; +const mockQueryConfig = { + identifier, + title: 'Mock Query', + query: statsQuery, + loadError: 'Failed to load mock query data', +}; + +const mockChartConfig = { + loadChartErrorMessage, + noDataMessage, + chartTitle: 'Foo', + yAxisTitle: 'Bar', + xAxisTitle: 'Baz', + queries: [mockQueryConfig], +}; + +describe('InstanceStatisticsCountChart', () => { + let wrapper; + let queryHandler; + + const createComponent = ({ responseHandler }) => { + return shallowMount(InstanceStatisticsCountChart, { + localVue, + apolloProvider: createMockApollo([[statsQuery, responseHandler]]), + propsData: { ...mockChartConfig }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findLoader = () => wrapper.find(ChartSkeletonLoader); + const findChart = () => wrapper.find(GlLineChart); + const findAlert = () => wrapper.find(GlAlert); + + describe('while loading', () => { + beforeEach(() => { + queryHandler = mockQueryResponse({ key: queryResponseDataKey, loading: true }); + wrapper = createComponent({ responseHandler: queryHandler }); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('displays the skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('without data', () => { + beforeEach(() => { + queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: [] }); + wrapper = createComponent({ responseHandler: queryHandler }); + }); + + it('renders an no data message', () => { + expect(findAlert().text()).toBe(noDataMessage); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('with data', () => { + beforeEach(() => { + queryHandler = mockQueryResponse({ key: queryResponseDataKey, data: mockCountsData1 }); + wrapper = createComponent({ responseHandler: queryHandler }); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when fetching more data', () => { + const recordedAt = '2020-08-01'; + describe('when the fetchMore query returns data', () => { + beforeEach(async () => { + const newData = [{ recordedAt, count: 5 }]; + queryHandler = mockQueryResponse({ + key: queryResponseDataKey, + data: mockCountsData1, + additionalData: newData, + }); + + wrapper = createComponent({ responseHandler: queryHandler }); + await wrapper.vm.$nextTick(); + }); + + it('requests data twice', () => { + expect(queryHandler).toBeCalledTimes(2); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + }); + + describe('when the fetchMore query throws an error', () => { + beforeEach(async () => { + queryHandler = jest.fn().mockResolvedValueOnce( + mockApolloResponse({ + key: queryResponseDataKey, + data: mockCountsData1, + hasNextPage: true, + }), + ); + + wrapper = createComponent({ responseHandler: queryHandler }); + jest + .spyOn(wrapper.vm.$apollo.queries[identifier], 'fetchMore') + .mockImplementation(jest.fn().mockRejectedValue()); + + await wrapper.vm.$nextTick(); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries[identifier].fetchMore).toHaveBeenCalledTimes(1); + }); + + it('show an error message', () => { + expect(findAlert().text()).toBe(loadChartErrorMessage); + }); + }); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js deleted file mode 100644 index a06d66f783e..00000000000 --- a/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js +++ /dev/null @@ -1,189 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { GlLineChart } from '@gitlab/ui/dist/charts'; -import { GlAlert } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'jest/helpers/mock_apollo_helper'; -import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; -import pipelinesStatsQuery from '~/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql'; -import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; -import { mockCountsData1, mockCountsData2 } from '../mock_data'; -import { getApolloResponse } from '../apollo_mock_data'; - -const localVue = createLocalVue(); -localVue.use(VueApollo); - -describe('PipelinesChart', () => { - let wrapper; - let queryHandler; - - const createApolloProvider = pipelineStatsHandler => { - return createMockApollo([[pipelinesStatsQuery, pipelineStatsHandler]]); - }; - - const createComponent = apolloProvider => { - return shallowMount(PipelinesChart, { - localVue, - apolloProvider, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const findLoader = () => wrapper.find(ChartSkeletonLoader); - const findChart = () => wrapper.find(GlLineChart); - const findAlert = () => wrapper.find(GlAlert); - - describe('while loading', () => { - beforeEach(() => { - queryHandler = jest.fn().mockReturnValue(new Promise(() => {})); - const apolloProvider = createApolloProvider(queryHandler); - wrapper = createComponent(apolloProvider); - }); - - it('requests data', () => { - expect(queryHandler).toBeCalledTimes(1); - }); - - it('displays the skeleton loader', () => { - expect(findLoader().exists()).toBe(true); - }); - - it('hides the chart', () => { - expect(findChart().exists()).toBe(false); - }); - - it('does not show an error', () => { - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('without data', () => { - beforeEach(() => { - const emptyResponse = getApolloResponse(); - queryHandler = jest.fn().mockResolvedValue(emptyResponse); - const apolloProvider = createApolloProvider(queryHandler); - wrapper = createComponent(apolloProvider); - }); - - it('renders an no data message', () => { - expect(findAlert().text()).toBe('There is no data available.'); - }); - - it('hides the skeleton loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('renders the chart', () => { - expect(findChart().exists()).toBe(false); - }); - }); - - describe('with data', () => { - beforeEach(() => { - const response = getApolloResponse({ - pipelinesTotal: mockCountsData1, - pipelinesSucceeded: mockCountsData2, - pipelinesFailed: mockCountsData2, - pipelinesCanceled: mockCountsData1, - pipelinesSkipped: mockCountsData1, - }); - queryHandler = jest.fn().mockResolvedValue(response); - const apolloProvider = createApolloProvider(queryHandler); - wrapper = createComponent(apolloProvider); - }); - - it('requests data', () => { - expect(queryHandler).toBeCalledTimes(1); - }); - - it('hides the skeleton loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('renders the chart', () => { - expect(findChart().exists()).toBe(true); - }); - - it('passes the data to the line chart', () => { - expect(findChart().props('data')).toMatchSnapshot(); - }); - - it('does not show an error', () => { - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('when fetching more data', () => { - const recordedAt = '2020-08-01'; - describe('when the fetchMore query returns data', () => { - beforeEach(async () => { - const newData = { recordedAt, count: 5 }; - const firstResponse = getApolloResponse({ - pipelinesTotal: mockCountsData2, - pipelinesSucceeded: mockCountsData2, - pipelinesFailed: mockCountsData1, - pipelinesCanceled: mockCountsData2, - pipelinesSkipped: mockCountsData2, - hasNextPage: true, - }); - const secondResponse = getApolloResponse({ - pipelinesTotal: [newData], - pipelinesSucceeded: [newData], - pipelinesFailed: [newData], - pipelinesCanceled: [newData], - pipelinesSkipped: [newData], - hasNextPage: false, - }); - queryHandler = jest - .fn() - .mockResolvedValueOnce(firstResponse) - .mockResolvedValueOnce(secondResponse); - const apolloProvider = createApolloProvider(queryHandler); - wrapper = createComponent(apolloProvider); - - await wrapper.vm.$nextTick(); - }); - - it('requests data twice', () => { - expect(queryHandler).toBeCalledTimes(2); - }); - - it('passes the data to the line chart', () => { - expect(findChart().props('data')).toMatchSnapshot(); - }); - }); - - describe('when the fetchMore query throws an error', () => { - beforeEach(async () => { - const response = getApolloResponse({ - pipelinesTotal: mockCountsData2, - pipelinesSucceeded: mockCountsData2, - pipelinesFailed: mockCountsData1, - pipelinesCanceled: mockCountsData2, - pipelinesSkipped: mockCountsData2, - hasNextPage: true, - }); - queryHandler = jest.fn().mockResolvedValue(response); - const apolloProvider = createApolloProvider(queryHandler); - wrapper = createComponent(apolloProvider); - jest - .spyOn(wrapper.vm.$apollo.queries.pipelineStats, 'fetchMore') - .mockImplementation(jest.fn().mockRejectedValue()); - await wrapper.vm.$nextTick(); - }); - - it('calls fetchMore', () => { - expect(wrapper.vm.$apollo.queries.pipelineStats.fetchMore).toHaveBeenCalledTimes(1); - }); - - it('show an error message', () => { - expect(findAlert().text()).toBe( - 'Could not load the pipelines chart. Please refresh the page to try again.', - ); - }); - }); - }); -}); diff --git a/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js new file mode 100644 index 00000000000..d9f42430aa8 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js @@ -0,0 +1,216 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import { useFakeDate } from 'helpers/fake_date'; +import ProjectsAndGroupChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import projectsQuery from '~/analytics/instance_statistics/graphql/queries/projects.query.graphql'; +import groupsQuery from '~/analytics/instance_statistics/graphql/queries/groups.query.graphql'; +import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data'; +import { mockQueryResponse } from '../apollo_mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('ProjectsAndGroupChart', () => { + let wrapper; + let queryResponses = { projects: null, groups: null }; + const mockAdditionalData = [{ recordedAt: '2020-07-21', count: 5 }]; + + const createComponent = ({ + loadingError = false, + projects = [], + groups = [], + projectsLoading = false, + groupsLoading = false, + projectsAdditionalData = [], + groupsAdditionalData = [], + } = {}) => { + queryResponses = { + projects: mockQueryResponse({ + key: 'projects', + data: projects, + loading: projectsLoading, + additionalData: projectsAdditionalData, + }), + groups: mockQueryResponse({ + key: 'groups', + data: groups, + loading: groupsLoading, + additionalData: groupsAdditionalData, + }), + }; + + return shallowMount(ProjectsAndGroupChart, { + props: { + startDate: useFakeDate(2020, 9, 26), + endDate: useFakeDate(2020, 10, 1), + totalDataPoints: mockCountsData2.length, + }, + localVue, + apolloProvider: createMockApollo([ + [projectsQuery, queryResponses.projects], + [groupsQuery, queryResponses.groups], + ]), + data() { + return { loadingError }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + queryResponses = { + projects: null, + groups: null, + }; + }); + + const findLoader = () => wrapper.find(ChartSkeletonLoader); + const findAlert = () => wrapper.find(GlAlert); + const findChart = () => wrapper.find(GlLineChart); + + describe('while loading', () => { + beforeEach(() => { + wrapper = createComponent({ projectsLoading: true, groupsLoading: true }); + }); + + it('displays the skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('while loading 1 data set', () => { + beforeEach(async () => { + wrapper = createComponent({ + projects: mockCountsData2, + groupsLoading: true, + }); + + await wrapper.vm.$nextTick(); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + }); + + describe('without data', () => { + beforeEach(async () => { + wrapper = createComponent({ projects: [] }); + await wrapper.vm.$nextTick(); + }); + + it('renders a no data message', () => { + expect(findAlert().text()).toBe('No data available.'); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('does not render the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('with data', () => { + beforeEach(async () => { + wrapper = createComponent({ projects: mockCountsData2 }); + await wrapper.vm.$nextTick(); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toEqual([ + { data: roundedSortedCountsMonthlyChartData2, name: 'Total projects' }, + { data: [], name: 'Total groups' }, + ]); + }); + }); + + describe('with errors', () => { + beforeEach(async () => { + wrapper = createComponent({ loadingError: true }); + await wrapper.vm.$nextTick(); + }); + + it('renders an error message', () => { + expect(findAlert().text()).toBe('No data available.'); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe.each` + metric | loadingState | newData + ${'projects'} | ${{ projectsAdditionalData: mockAdditionalData }} | ${{ projects: mockCountsData2 }} + ${'groups'} | ${{ groupsAdditionalData: mockAdditionalData }} | ${{ groups: mockCountsData2 }} + `('$metric - fetchMore', ({ metric, loadingState, newData }) => { + describe('when the fetchMore query returns data', () => { + beforeEach(async () => { + wrapper = createComponent({ + ...loadingState, + ...newData, + }); + + jest.spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore'); + await wrapper.vm.$nextTick(); + }); + + it('requests data twice', () => { + expect(queryResponses[metric]).toBeCalledTimes(2); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the fetchMore query throws an error', () => { + beforeEach(() => { + wrapper = createComponent({ + ...loadingState, + ...newData, + }); + + jest + .spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore') + .mockImplementation(jest.fn().mockRejectedValue()); + return wrapper.vm.$nextTick(); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1); + }); + + it('renders an error message', () => { + expect(findAlert().text()).toBe('No data available.'); + }); + }); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js index 7509c1e6626..6ed9d203f3d 100644 --- a/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js +++ b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js @@ -7,7 +7,12 @@ import { useFakeDate } from 'helpers/fake_date'; import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql'; -import { mockCountsData2, roundedSortedCountsMonthlyChartData2, mockPageInfo } from '../mock_data'; +import { + mockCountsData1, + mockCountsData2, + roundedSortedCountsMonthlyChartData2, +} from '../mock_data'; +import { mockQueryResponse } from '../apollo_mock_data'; const localVue = createLocalVue(); localVue.use(VueApollo); @@ -16,43 +21,13 @@ describe('UsersChart', () => { let wrapper; let queryHandler; - const mockApolloResponse = ({ loading = false, hasNextPage = false, users }) => ({ - data: { - users: { - pageInfo: { ...mockPageInfo, hasNextPage }, - nodes: users, - loading, - }, - }, - }); - - const mockQueryResponse = ({ users, loading = false, hasNextPage = false }) => { - const apolloQueryResponse = mockApolloResponse({ loading, hasNextPage, users }); - if (loading) { - return jest.fn().mockReturnValue(new Promise(() => {})); - } - if (hasNextPage) { - return jest - .fn() - .mockResolvedValueOnce(apolloQueryResponse) - .mockResolvedValueOnce( - mockApolloResponse({ - loading, - hasNextPage: false, - users: [{ recordedAt: '2020-07-21', count: 5 }], - }), - ); - } - return jest.fn().mockResolvedValue(apolloQueryResponse); - }; - const createComponent = ({ loadingError = false, loading = false, users = [], - hasNextPage = false, + additionalData = [], } = {}) => { - queryHandler = mockQueryResponse({ users, loading, hasNextPage }); + queryHandler = mockQueryResponse({ key: 'users', data: users, loading, additionalData }); return shallowMount(UsersChart, { props: { @@ -157,7 +132,7 @@ describe('UsersChart', () => { beforeEach(async () => { wrapper = createComponent({ users: mockCountsData2, - hasNextPage: true, + additionalData: mockCountsData1, }); jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore'); @@ -177,7 +152,7 @@ describe('UsersChart', () => { beforeEach(() => { wrapper = createComponent({ users: mockCountsData2, - hasNextPage: true, + additionalData: mockCountsData1, }); jest diff --git a/spec/frontend/analytics/instance_statistics/mock_data.js b/spec/frontend/analytics/instance_statistics/mock_data.js index b737db4c55f..e86e552a952 100644 --- a/spec/frontend/analytics/instance_statistics/mock_data.js +++ b/spec/frontend/analytics/instance_statistics/mock_data.js @@ -33,10 +33,3 @@ export const roundedSortedCountsMonthlyChartData2 = [ ['2020-06-01', 21], // average of 2020-06-x items ['2020-07-01', 10], // average of 2020-07-x items ]; - -export const mockPageInfo = { - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - endCursor: null, -}; diff --git a/spec/frontend/analytics/instance_statistics/utils_spec.js b/spec/frontend/analytics/instance_statistics/utils_spec.js index d480238419b..3fd89c7f740 100644 --- a/spec/frontend/analytics/instance_statistics/utils_spec.js +++ b/spec/frontend/analytics/instance_statistics/utils_spec.js @@ -1,7 +1,7 @@ import { getAverageByMonth, - extractValues, - sortByDate, + getEarliestDate, + generateDataKeys, } from '~/analytics/instance_statistics/utils'; import { mockCountsData1, @@ -44,41 +44,38 @@ describe('getAverageByMonth', () => { }); }); -describe('extractValues', () => { - it('extracts only requested values', () => { - const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' }; - expect(extractValues(data, ['fooBar'], 'foo', 'baz')).toEqual({ bazBar: 'quis' }); +describe('getEarliestDate', () => { + it('returns the date of the final item in the array', () => { + expect(getEarliestDate(mockCountsData1)).toBe('2020-06-12'); }); - it('is able to extract multiple values', () => { - const data = { - fooBar: { baz: 'quis' }, - fooBaz: { baz: 'quis' }, - fooQuis: { baz: 'quis' }, - }; - expect(extractValues(data, ['fooBar', 'fooBaz', 'fooQuis'], 'foo', 'baz')).toEqual({ - bazBar: 'quis', - bazBaz: 'quis', - bazQuis: 'quis', - }); - }); - - it('returns empty data set when keys are not found', () => { - const data = { foo: { baz: 'quis' }, ignored: 'ignored' }; - expect(extractValues(data, ['fooBar'], 'foo', 'baz')).toEqual({}); + it('returns null for an empty array', () => { + expect(getEarliestDate([])).toBeNull(); }); - it('returns empty data when params are missing', () => { - expect(extractValues()).toEqual({}); + it("returns null if the array has data but `recordedAt` isn't defined", () => { + expect( + getEarliestDate(mockCountsData1.map(({ recordedAt: date, ...rest }) => ({ date, ...rest }))), + ).toBeNull(); }); }); -describe('sortByDate', () => { - it('sorts the array by date', () => { - expect(sortByDate(mockCountsData1)).toStrictEqual([...mockCountsData1].reverse()); +describe('generateDataKeys', () => { + const fakeQueries = [ + { identifier: 'from' }, + { identifier: 'first' }, + { identifier: 'to' }, + { identifier: 'last' }, + ]; + + const defaultValue = 'default value'; + const res = generateDataKeys(fakeQueries, defaultValue); + + it('extracts each query identifier and sets them as object keys', () => { + expect(Object.keys(res)).toEqual(['from', 'first', 'to', 'last']); }); - it('does not modify the original array', () => { - expect(sortByDate(countsMonthlyChartData1)).not.toBe(countsMonthlyChartData1); + it('sets every value to the `defaultValue` provided', () => { + expect(Object.values(res)).toEqual(Array(fakeQueries.length).fill(defaultValue)); }); }); |