diff options
Diffstat (limited to 'spec/frontend/analytics')
-rw-r--r-- | spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js | 61 | ||||
-rw-r--r-- | spec/frontend/analytics/shared/components/daterange_spec.js | 120 | ||||
-rw-r--r-- | spec/frontend/analytics/shared/components/metric_card_spec.js | 129 | ||||
-rw-r--r-- | spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js | 264 | ||||
-rw-r--r-- | spec/frontend/analytics/shared/utils_spec.js | 24 | ||||
-rw-r--r-- | spec/frontend/analytics/usage_trends/components/usage_counts_spec.js (renamed from spec/frontend/analytics/usage_trends/components/instance_counts_spec.js) | 22 |
6 files changed, 484 insertions, 136 deletions
diff --git a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js new file mode 100644 index 00000000000..75ef9d9db94 --- /dev/null +++ b/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js @@ -0,0 +1,61 @@ +import { GlEmptyState, GlSprintf } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue'; + +describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + const createWrapper = ({ isAdmin = false } = {}) => { + wrapper = shallowMountExtended(ServicePingDisabled, { + provide: { + isAdmin, + svgPath: TEST_HOST, + docsLink: TEST_HOST, + primaryButtonPath: TEST_HOST, + }, + stubs: { GlEmptyState, GlSprintf }, + }); + }; + + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findMessageForRegularUsers = () => wrapper.findComponent(GlSprintf); + const findDocsLink = () => wrapper.findByTestId('docs-link'); + const findPowerOnButton = () => wrapper.findByTestId('power-on-button'); + + it('renders empty state with provided SVG path', () => { + createWrapper(); + + expect(findEmptyState().props('svgPath')).toBe(TEST_HOST); + }); + + describe('for regular users', () => { + beforeEach(() => { + createWrapper({ isAdmin: false }); + }); + + it('renders message without power-on button', () => { + expect(findMessageForRegularUsers().exists()).toBe(true); + expect(findPowerOnButton().exists()).toBe(false); + }); + + it('renders docs link', () => { + expect(findDocsLink().exists()).toBe(true); + expect(findDocsLink().attributes('href')).toBe(TEST_HOST); + }); + }); + + describe('for admins', () => { + beforeEach(() => { + createWrapper({ isAdmin: true }); + }); + + it('renders power-on button', () => { + expect(findPowerOnButton().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js new file mode 100644 index 00000000000..854582abb82 --- /dev/null +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -0,0 +1,120 @@ +import { GlDaterangePicker } from '@gitlab/ui'; +import { 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 = { + startDate: new Date(2019, 8, 1), + endDate: new Date(2019, 8, 11), +}; + +describe('Daterange component', () => { + useFakeDate(2019, 8, 25); + + let wrapper; + + const factory = (props = defaultProps) => { + wrapper = mount(Daterange, { + propsData: { + ...defaultProps, + ...props, + }, + directives: { GlTooltip: createMockDirective() }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findDaterangePicker = () => wrapper.find(GlDaterangePicker); + + const findDateRangeIndicator = () => wrapper.find('.daterange-indicator'); + + describe('template', () => { + describe('when show is false', () => { + it('does not render the daterange picker', () => { + factory({ show: false }); + expect(findDaterangePicker().exists()).toBe(false); + }); + }); + + 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', () => { + 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 }); + + const input = findDaterangePicker().find('input'); + + input.setValue('2019-01-01'); + input.trigger('change'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]); + }); + }); + }); + + describe('with a maxDateRange being set', () => { + beforeEach(() => { + factory({ maxDateRange: 30 }); + }); + + it('displays the max date range indicator', () => { + expect(findDateRangeIndicator().exists()).toBe(true); + }); + + it('displays the correct number of selected days in the indicator', () => { + expect(findDateRangeIndicator().find('span').text()).toBe('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.', + ); + }); + }); + }); + + describe('computed', () => { + describe('dateRange', () => { + beforeEach(() => { + factory({ show: true }); + }); + + describe('set', () => { + it('emits the change event with an object containing startDate and endDate', () => { + const startDate = new Date('2019-10-01'); + const endDate = new Date('2019-10-05'); + wrapper.vm.dateRange = { startDate, endDate }; + + expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]); + }); + }); + + describe('get', () => { + it("returns value of dateRange from state's startDate and endDate", () => { + expect(wrapper.vm.dateRange).toEqual({ + startDate: defaultProps.startDate, + endDate: defaultProps.endDate, + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/analytics/shared/components/metric_card_spec.js b/spec/frontend/analytics/shared/components/metric_card_spec.js deleted file mode 100644 index 7f587d227ab..00000000000 --- a/spec/frontend/analytics/shared/components/metric_card_spec.js +++ /dev/null @@ -1,129 +0,0 @@ -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import MetricCard from '~/analytics/shared/components/metric_card.vue'; - -const metrics = [ - { key: 'first_metric', value: 10, label: 'First metric', unit: 'days', link: 'some_link' }, - { key: 'second_metric', value: 20, label: 'Yet another metric' }, - { key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' }, - { key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' }, -]; - -const defaultProps = { - title: 'My fancy title', - isLoading: false, - metrics, -}; - -describe('MetricCard', () => { - let wrapper; - - const factory = (props = defaultProps) => { - wrapper = mount(MetricCard, { - propsData: { - ...defaultProps, - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findTitle = () => wrapper.find({ ref: 'title' }); - const findLoadingIndicator = () => wrapper.find(GlSkeletonLoading); - const findMetricsWrapper = () => wrapper.find({ ref: 'metricsWrapper' }); - const findMetricItem = () => wrapper.findAll({ ref: 'metricItem' }); - const findTooltip = () => wrapper.find('[data-testid="tooltip"]'); - - describe('template', () => { - it('renders the title', () => { - factory(); - - expect(findTitle().text()).toContain('My fancy title'); - }); - - describe('when isLoading is true', () => { - beforeEach(() => { - factory({ isLoading: true }); - }); - - it('displays a loading indicator', () => { - expect(findLoadingIndicator().exists()).toBe(true); - }); - - it('does not display the metrics container', () => { - expect(findMetricsWrapper().exists()).toBe(false); - }); - }); - - describe('when isLoading is false', () => { - beforeEach(() => { - factory({ isLoading: false }); - }); - - it('does not display a loading indicator', () => { - expect(findLoadingIndicator().exists()).toBe(false); - }); - - it('displays the metrics container', () => { - expect(findMetricsWrapper().exists()).toBe(true); - }); - - it('renders two metrics', () => { - expect(findMetricItem()).toHaveLength(metrics.length); - }); - - describe('with tooltip text', () => { - const tooltipText = 'This is a tooltip'; - const tooltipMetric = { - key: 'fifth_metric', - value: '-', - label: 'Metric with tooltip', - unit: 'parsecs', - tooltipText, - }; - - beforeEach(() => { - factory({ - isLoading: false, - metrics: [tooltipMetric], - }); - }); - - it('will render a tooltip', () => { - const tt = getBinding(findTooltip().element, 'gl-tooltip'); - expect(tt.value.title).toEqual(tooltipText); - }); - }); - - describe.each` - columnIndex | label | value | unit | link - ${0} | ${'First metric'} | ${10} | ${' days'} | ${'some_link'} - ${1} | ${'Yet another metric'} | ${20} | ${''} | ${null} - ${2} | ${'Null metric without value'} | ${'-'} | ${''} | ${null} - ${3} | ${'Metric without value'} | ${'-'} | ${''} | ${null} - `('metric columns', ({ columnIndex, label, value, unit, link }) => { - it(`renders ${value}${unit} ${label} with URL ${link}`, () => { - const allMetricItems = findMetricItem(); - const metricItem = allMetricItems.at(columnIndex); - const text = metricItem.text(); - - expect(text).toContain(`${value}${unit}`); - expect(text).toContain(label); - - if (link) { - expect(metricItem.find('a').attributes('href')).toBe(link); - } else { - expect(metricItem.find('a').exists()).toBe(false); - } - }); - }); - }); - }); -}); diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js new file mode 100644 index 00000000000..2537b8fb816 --- /dev/null +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -0,0 +1,264 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { TEST_HOST } from 'helpers/test_constants'; +import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; +import getProjects from '~/analytics/shared/graphql/projects.query.graphql'; + +const projects = [ + { + id: 'gid://gitlab/Project/1', + name: 'Gitlab Test', + fullPath: 'gitlab-org/gitlab-test', + avatarUrl: `${TEST_HOST}/images/home/nasa.svg`, + }, + { + id: 'gid://gitlab/Project/2', + name: 'Gitlab Shell', + fullPath: 'gitlab-org/gitlab-shell', + avatarUrl: null, + }, + { + id: 'gid://gitlab/Project/3', + name: 'Foo', + fullPath: 'gitlab-org/foo', + avatarUrl: null, + }, +]; + +const defaultMocks = { + $apollo: { + query: jest.fn().mockResolvedValue({ + data: { group: { projects: { nodes: projects } } }, + }), + }, +}; + +let spyQuery; + +describe('ProjectsDropdownFilter component', () => { + let wrapper; + + const createComponent = (props = {}) => { + spyQuery = defaultMocks.$apollo.query; + wrapper = mount(ProjectsDropdownFilter, { + mocks: { ...defaultMocks }, + propsData: { + groupId: 1, + groupNamespace: 'gitlab-org', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findDropdown = () => wrapper.find(GlDropdown); + + const findDropdownItems = () => + findDropdown() + .findAll(GlDropdownItem) + .filter((w) => w.text() !== 'No matching results'); + + const findDropdownAtIndex = (index) => findDropdownItems().at(index); + + const findDropdownButton = () => findDropdown().find('.dropdown-toggle'); + const findDropdownButtonAvatar = () => findDropdown().find('.gl-avatar'); + const findDropdownButtonAvatarAtIndex = (index) => + findDropdownAtIndex(index).find('img.gl-avatar'); + const findDropdownButtonIdentIconAtIndex = (index) => + findDropdownAtIndex(index).find('div.gl-avatar-identicon'); + + const findDropdownNameAtIndex = (index) => + findDropdownAtIndex(index).find('[data-testid="project-name"'); + const findDropdownFullPathAtIndex = (index) => + findDropdownAtIndex(index).find('[data-testid="project-full-path"]'); + + const selectDropdownItemAtIndex = (index) => + findDropdownAtIndex(index).find('button').trigger('click'); + + const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id); + + describe('queryParams are applied when fetching data', () => { + beforeEach(() => { + createComponent({ + queryParams: { + first: 50, + includeSubgroups: true, + }, + }); + }); + + it('applies the correct queryParams when making an api call', async () => { + wrapper.setData({ searchTerm: 'gitlab' }); + + expect(spyQuery).toHaveBeenCalledTimes(1); + + await wrapper.vm.$nextTick(() => { + expect(spyQuery).toHaveBeenCalledWith({ + query: getProjects, + variables: { + search: 'gitlab', + groupFullPath: wrapper.vm.groupNamespace, + first: 50, + includeSubgroups: true, + }, + }); + }); + }); + }); + + describe('when passed a an array of defaultProject as prop', () => { + beforeEach(() => { + createComponent({ + defaultProjects: [projects[0]], + }); + }); + + it("displays the defaultProject's name", () => { + expect(findDropdownButton().text()).toContain(projects[0].name); + }); + + it("renders the defaultProject's avatar", () => { + expect(findDropdownButtonAvatar().exists()).toBe(true); + }); + + it('marks the defaultProject as selected', () => { + expect(findDropdownAtIndex(0).props('isChecked')).toBe(true); + }); + }); + + describe('when multiSelect is false', () => { + beforeEach(() => { + createComponent({ multiSelect: false }); + }); + + describe('displays the correct information', () => { + it('contains 3 items', () => { + expect(findDropdownItems()).toHaveLength(3); + }); + + it('renders an avatar when the project has an avatarUrl', () => { + expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); + expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); + }); + + it("renders an identicon when the project doesn't have an avatarUrl", () => { + expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false); + expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true); + }); + + it('renders the project name', () => { + projects.forEach((project, index) => { + expect(findDropdownNameAtIndex(index).text()).toBe(project.name); + }); + }); + + it('renders the project fullPath', () => { + projects.forEach((project, index) => { + expect(findDropdownFullPathAtIndex(index).text()).toBe(project.fullPath); + }); + }); + }); + + describe('on project click', () => { + it('should emit the "selected" event with the selected project', () => { + selectDropdownItemAtIndex(0); + + expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]); + }); + + it('should change selection when new project is clicked', () => { + selectDropdownItemAtIndex(1); + + expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]); + }); + + it('selection should be emptied when a project is deselected', () => { + selectDropdownItemAtIndex(0); // Select the item + selectDropdownItemAtIndex(0); // deselect it + + expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]); + }); + + it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => { + selectDropdownItemAtIndex(0); + + await wrapper.vm.$nextTick().then(() => { + expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); + expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); + }); + }); + + it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => { + selectDropdownItemAtIndex(1); + + await wrapper.vm.$nextTick().then(() => { + expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false); + expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true); + }); + }); + }); + }); + + describe('when multiSelect is true', () => { + beforeEach(() => { + createComponent({ multiSelect: true }); + }); + + describe('displays the correct information', () => { + it('contains 3 items', () => { + expect(findDropdownItems()).toHaveLength(3); + }); + + it('renders an avatar when the project has an avatarUrl', () => { + expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); + expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); + }); + + it("renders an identicon when the project doesn't have an avatarUrl", () => { + expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false); + expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true); + }); + + it('renders the project name', () => { + projects.forEach((project, index) => { + expect(findDropdownNameAtIndex(index).text()).toBe(project.name); + }); + }); + + it('renders the project fullPath', () => { + projects.forEach((project, index) => { + expect(findDropdownFullPathAtIndex(index).text()).toBe(project.fullPath); + }); + }); + }); + + describe('on project click', () => { + it('should add to selection when new project is clicked', () => { + selectDropdownItemAtIndex(0); + selectDropdownItemAtIndex(1); + + expect(selectedIds()).toEqual([projects[0].id, projects[1].id]); + }); + + it('should remove from selection when clicked again', () => { + selectDropdownItemAtIndex(0); + expect(selectedIds()).toEqual([projects[0].id]); + + selectDropdownItemAtIndex(0); + expect(selectedIds()).toEqual([]); + }); + + it('renders the correct placeholder text when multiple projects are selected', async () => { + selectDropdownItemAtIndex(0); + selectDropdownItemAtIndex(1); + + await wrapper.vm.$nextTick().then(() => { + 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 new file mode 100644 index 00000000000..e3293f2d8bd --- /dev/null +++ b/spec/frontend/analytics/shared/utils_spec.js @@ -0,0 +1,24 @@ +import { filterBySearchTerm } from '~/analytics/shared/utils'; + +describe('filterBySearchTerm', () => { + const data = [ + { name: 'eins', title: 'one' }, + { name: 'zwei', title: 'two' }, + { name: 'drei', title: 'three' }, + ]; + const searchTerm = 'rei'; + + it('filters data by `name` for the provided search term', () => { + expect(filterBySearchTerm(data, searchTerm)).toEqual([data[2]]); + }); + + it('with no search term returns the data', () => { + ['', null].forEach((search) => { + expect(filterBySearchTerm(data, search)).toEqual(data); + }); + }); + + it('with a key, filters by the provided key', () => { + expect(filterBySearchTerm(data, 'ne', 'title')).toEqual([data[0]]); + }); +}); diff --git a/spec/frontend/analytics/usage_trends/components/instance_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js index 707d2cc310f..703767dab47 100644 --- a/spec/frontend/analytics/usage_trends/components/instance_counts_spec.js +++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js @@ -1,5 +1,6 @@ +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { shallowMount } from '@vue/test-utils'; -import MetricCard from '~/analytics/shared/components/metric_card.vue'; import UsageCounts from '~/analytics/usage_trends/components/usage_counts.vue'; import { mockUsageCounts } from '../mock_data'; @@ -27,18 +28,18 @@ describe('UsageCounts', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); - const findMetricCard = () => wrapper.find(MetricCard); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoading); + const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat); describe('while loading', () => { beforeEach(() => { createComponent({ loading: true }); }); - it('displays the metric card with isLoading=true', () => { - expect(findMetricCard().props('isLoading')).toBe(true); + it('displays a loading indicator', () => { + expect(findSkeletonLoader().exists()).toBe(true); }); }); @@ -47,8 +48,15 @@ describe('UsageCounts', () => { createComponent({ data: { counts: mockUsageCounts } }); }); - it('passes the counts data to the metric card', () => { - expect(findMetricCard().props('metrics')).toEqual(mockUsageCounts); + it.each` + index | value | title + ${0} | ${mockUsageCounts[0].value} | ${mockUsageCounts[0].label} + ${1} | ${mockUsageCounts[1].value} | ${mockUsageCounts[1].label} + `('renders a GlSingleStat for "$title"', ({ index, value, title }) => { + const singleStat = findAllSingleStats().at(index); + + expect(singleStat.props('value')).toBe(`${value}`); + expect(singleStat.props('title')).toBe(title); }); }); }); |