706 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			706 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import Vuex from 'vuex';
 | |
| import { shallowMount } from '@vue/test-utils';
 | |
| import AxiosMockAdapter from 'axios-mock-adapter';
 | |
| import { setTestTimeout } from 'helpers/timeout';
 | |
| import invalidUrl from '~/lib/utils/invalid_url';
 | |
| import axios from '~/lib/utils/axios_utils';
 | |
| import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
 | |
| import AlertWidget from '~/monitoring/components/alert_widget.vue';
 | |
| 
 | |
| import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
 | |
| import {
 | |
|   mockLogsHref,
 | |
|   mockLogsPath,
 | |
|   mockNamespace,
 | |
|   mockNamespacedData,
 | |
|   mockTimeRange,
 | |
|   graphDataPrometheusQueryRangeMultiTrack,
 | |
|   barMockData,
 | |
| } from '../mock_data';
 | |
| import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
 | |
| import { anomalyGraphData, singleStatGraphData } from '../graph_data';
 | |
| 
 | |
| import { panelTypes } from '~/monitoring/constants';
 | |
| 
 | |
| import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
 | |
| import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
 | |
| import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
 | |
| import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
 | |
| import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
 | |
| import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
 | |
| import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
 | |
| import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
 | |
| 
 | |
| import { createStore, monitoringDashboard } from '~/monitoring/stores';
 | |
| import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
 | |
| 
 | |
| global.URL.createObjectURL = jest.fn();
 | |
| 
 | |
| const mocks = {
 | |
|   $toast: {
 | |
|     show: jest.fn(),
 | |
|   },
 | |
| };
 | |
| 
 | |
| describe('Dashboard Panel', () => {
 | |
|   let axiosMock;
 | |
|   let store;
 | |
|   let state;
 | |
|   let wrapper;
 | |
| 
 | |
|   const exampleText = 'example_text';
 | |
| 
 | |
|   const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
 | |
|   const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
 | |
|   const findTitle = () => wrapper.find({ ref: 'graphTitle' });
 | |
|   const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
 | |
|   const findMenuItems = () => wrapper.findAll(GlDropdownItem);
 | |
|   const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text);
 | |
| 
 | |
|   const createWrapper = (props, options) => {
 | |
|     wrapper = shallowMount(DashboardPanel, {
 | |
|       propsData: {
 | |
|         graphData,
 | |
|         settingsPath: dashboardProps.settingsPath,
 | |
|         ...props,
 | |
|       },
 | |
|       store,
 | |
|       mocks,
 | |
|       ...options,
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   const mockGetterReturnValue = (getter, value) => {
 | |
|     jest.spyOn(monitoringDashboard.getters, getter).mockReturnValue(value);
 | |
|     store = new Vuex.Store({
 | |
|       modules: {
 | |
|         monitoringDashboard,
 | |
|       },
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   beforeEach(() => {
 | |
|     setTestTimeout(1000);
 | |
| 
 | |
|     store = createStore();
 | |
|     state = store.state.monitoringDashboard;
 | |
| 
 | |
|     axiosMock = new AxiosMockAdapter(axios);
 | |
|   });
 | |
| 
 | |
|   afterEach(() => {
 | |
|     axiosMock.reset();
 | |
|   });
 | |
| 
 | |
|   describe('Renders slots', () => {
 | |
|     it('renders "topLeft" slot', () => {
 | |
|       createWrapper(
 | |
|         {},
 | |
|         {
 | |
|           slots: {
 | |
|             topLeft: `<div class="top-left-content">OK</div>`,
 | |
|           },
 | |
|         },
 | |
|       );
 | |
| 
 | |
|       expect(wrapper.find('.top-left-content').exists()).toBe(true);
 | |
|       expect(wrapper.find('.top-left-content').text()).toBe('OK');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('When no graphData is available', () => {
 | |
|     beforeEach(() => {
 | |
|       createWrapper({
 | |
|         graphData: graphDataEmpty,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       wrapper.destroy();
 | |
|     });
 | |
| 
 | |
|     it('renders the chart title', () => {
 | |
|       expect(findTitle().text()).toBe(graphDataEmpty.title);
 | |
|     });
 | |
| 
 | |
|     it('renders no download csv link', () => {
 | |
|       expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('does not contain graph widgets', () => {
 | |
|       expect(findCtxMenu().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('The Empty Chart component is rendered and is a Vue instance', () => {
 | |
|       expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
 | |
|       expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('When graphData is null', () => {
 | |
|     beforeEach(() => {
 | |
|       createWrapper({
 | |
|         graphData: null,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       wrapper.destroy();
 | |
|     });
 | |
| 
 | |
|     it('renders no chart title', () => {
 | |
|       expect(findTitle().text()).toBe('');
 | |
|     });
 | |
| 
 | |
|     it('renders no download csv link', () => {
 | |
|       expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('does not contain graph widgets', () => {
 | |
|       expect(findCtxMenu().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('The Empty Chart component is rendered and is a Vue instance', () => {
 | |
|       expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
 | |
|       expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('When graphData is available', () => {
 | |
|     beforeEach(() => {
 | |
|       createWrapper();
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       wrapper.destroy();
 | |
|     });
 | |
| 
 | |
|     it('renders the chart title', () => {
 | |
|       expect(findTitle().text()).toBe(graphData.title);
 | |
|     });
 | |
| 
 | |
|     it('contains graph widgets', () => {
 | |
|       expect(findCtxMenu().exists()).toBe(true);
 | |
|       expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('sets no clipboard copy link on dropdown by default', () => {
 | |
|       expect(findCopyLink().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('should emit `timerange` event when a zooming in/out in a chart occcurs', () => {
 | |
|       const timeRange = {
 | |
|         start: '2020-01-01T00:00:00.000Z',
 | |
|         end: '2020-01-01T01:00:00.000Z',
 | |
|       };
 | |
| 
 | |
|       jest.spyOn(wrapper.vm, '$emit');
 | |
| 
 | |
|       findTimeChart().vm.$emit('datazoom', timeRange);
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('includes a default group id', () => {
 | |
|       expect(wrapper.vm.groupId).toBe('dashboard-panel');
 | |
|     });
 | |
| 
 | |
|     describe('Supports different panel types', () => {
 | |
|       const dataWithType = type => {
 | |
|         return {
 | |
|           ...graphData,
 | |
|           type,
 | |
|         };
 | |
|       };
 | |
| 
 | |
|       it('empty chart is rendered for empty results', () => {
 | |
|         createWrapper({ graphData: graphDataEmpty });
 | |
|         expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
 | |
|         expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('area chart is rendered by default', () => {
 | |
|         createWrapper();
 | |
|         expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
 | |
|         expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
 | |
|       });
 | |
| 
 | |
|       describe.each`
 | |
|         data                                       | component                    | hasCtxMenu
 | |
|         ${dataWithType(panelTypes.AREA_CHART)}     | ${MonitorTimeSeriesChart}    | ${true}
 | |
|         ${dataWithType(panelTypes.LINE_CHART)}     | ${MonitorTimeSeriesChart}    | ${true}
 | |
|         ${singleStatGraphData()}                   | ${MonitorSingleStatChart}    | ${true}
 | |
|         ${anomalyGraphData()}                      | ${MonitorAnomalyChart}       | ${false}
 | |
|         ${dataWithType(panelTypes.COLUMN)}         | ${MonitorColumnChart}        | ${false}
 | |
|         ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
 | |
|         ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}       | ${false}
 | |
|         ${barMockData}                             | ${MonitorBarChart}           | ${false}
 | |
|       `('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
 | |
|         const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
 | |
| 
 | |
|         beforeEach(() => {
 | |
|           createWrapper({ graphData: data }, { attrs });
 | |
|         });
 | |
| 
 | |
|         it(`renders the chart component and binds attributes`, () => {
 | |
|           expect(wrapper.find(component).exists()).toBe(true);
 | |
|           expect(wrapper.find(component).isVueInstance()).toBe(true);
 | |
|           expect(wrapper.find(component).attributes()).toMatchObject(attrs);
 | |
|         });
 | |
| 
 | |
|         it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
 | |
|           expect(findCtxMenu().exists()).toBe(hasCtxMenu);
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('Edit custom metric dropdown item', () => {
 | |
|     const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
 | |
|     const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       createWrapper();
 | |
| 
 | |
|       return wrapper.vm.$nextTick();
 | |
|     });
 | |
| 
 | |
|     it('is not present if the panel is not a custom metric', () => {
 | |
|       expect(findEditCustomMetricLink().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('is present when the panel contains an edit_path property', () => {
 | |
|       wrapper.setProps({
 | |
|         graphData: {
 | |
|           ...graphData,
 | |
|           metrics: [
 | |
|             {
 | |
|               ...graphData.metrics[0],
 | |
|               edit_path: mockEditPath,
 | |
|             },
 | |
|           ],
 | |
|         },
 | |
|       });
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findEditCustomMetricLink().exists()).toBe(true);
 | |
|         expect(findEditCustomMetricLink().text()).toBe('Edit metric');
 | |
|         expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', () => {
 | |
|       wrapper.setProps({
 | |
|         graphData: {
 | |
|           ...graphData,
 | |
|           metrics: [
 | |
|             {
 | |
|               ...graphData.metrics[0],
 | |
|               edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
 | |
|             },
 | |
|             {
 | |
|               ...graphData.metrics[0],
 | |
|               edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
 | |
|             },
 | |
|           ],
 | |
|         },
 | |
|       });
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
 | |
|         expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('View Logs dropdown item', () => {
 | |
|     const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       createWrapper();
 | |
|       return wrapper.vm.$nextTick();
 | |
|     });
 | |
| 
 | |
|     it('is not present by default', () =>
 | |
|       wrapper.vm.$nextTick(() => {
 | |
|         expect(findViewLogsLink().exists()).toBe(false);
 | |
|       }));
 | |
| 
 | |
|     it('is not present if a time range is not set', () => {
 | |
|       state.logsPath = mockLogsPath;
 | |
|       state.timeRange = null;
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findViewLogsLink().exists()).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('is not present if the logs path is default', () => {
 | |
|       state.logsPath = invalidUrl;
 | |
|       state.timeRange = mockTimeRange;
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findViewLogsLink().exists()).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('is not present if the logs path is not set', () => {
 | |
|       state.logsPath = null;
 | |
|       state.timeRange = mockTimeRange;
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findViewLogsLink().exists()).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('is present when logs path and time a range is present', () => {
 | |
|       state.logsPath = mockLogsPath;
 | |
|       state.timeRange = mockTimeRange;
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('it is overridden when a datazoom event is received', () => {
 | |
|       state.logsPath = mockLogsPath;
 | |
|       state.timeRange = mockTimeRange;
 | |
| 
 | |
|       const zoomedTimeRange = {
 | |
|         start: '2020-01-01T00:00:00.000Z',
 | |
|         end: '2020-01-01T01:00:00.000Z',
 | |
|       };
 | |
| 
 | |
|       findTimeChart().vm.$emit('datazoom', zoomedTimeRange);
 | |
| 
 | |
|       return wrapper.vm.$nextTick(() => {
 | |
|         const start = encodeURIComponent(zoomedTimeRange.start);
 | |
|         const end = encodeURIComponent(zoomedTimeRange.end);
 | |
|         expect(findViewLogsLink().attributes('href')).toMatch(
 | |
|           `${mockLogsPath}?start=${start}&end=${end}`,
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when clipboard data is available', () => {
 | |
|     const clipboardText = 'A value to copy.';
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       createWrapper({
 | |
|         clipboardText,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('sets clipboard text on the dropdown', () => {
 | |
|       expect(findCopyLink().exists()).toBe(true);
 | |
|       expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
 | |
|     });
 | |
| 
 | |
|     it('adds a copy button to the dropdown', () => {
 | |
|       expect(findCopyLink().text()).toContain('Copy link to chart');
 | |
|     });
 | |
| 
 | |
|     it('opens a toast on click', () => {
 | |
|       findCopyLink().vm.$emit('click');
 | |
| 
 | |
|       expect(wrapper.vm.$toast.show).toHaveBeenCalled();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when clipboard data is not available', () => {
 | |
|     it('there is no "copy to clipboard" link for a null value', () => {
 | |
|       createWrapper({ clipboardText: null });
 | |
|       expect(findCopyLink().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('there is no "copy to clipboard" link for an empty value', () => {
 | |
|       createWrapper({ clipboardText: '' });
 | |
|       expect(findCopyLink().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when downloading metrics data as CSV', () => {
 | |
|     beforeEach(() => {
 | |
|       wrapper = shallowMount(DashboardPanel, {
 | |
|         propsData: {
 | |
|           clipboardText: exampleText,
 | |
|           settingsPath: dashboardProps.settingsPath,
 | |
|           graphData: {
 | |
|             y_label: 'metric',
 | |
|             ...graphData,
 | |
|           },
 | |
|         },
 | |
|         store,
 | |
|       });
 | |
|       return wrapper.vm.$nextTick();
 | |
|     });
 | |
| 
 | |
|     afterEach(() => {
 | |
|       wrapper.destroy();
 | |
|     });
 | |
| 
 | |
|     describe('csvText', () => {
 | |
|       it('converts metrics data from json to csv', () => {
 | |
|         const header = `timestamp,${graphData.y_label}`;
 | |
|         const data = graphData.metrics[0].result[0].values;
 | |
|         const firstRow = `${data[0][0]},${data[0][1]}`;
 | |
|         const secondRow = `${data[1][0]},${data[1][1]}`;
 | |
| 
 | |
|         expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('downloadCsv', () => {
 | |
|       it('produces a link with a Blob', () => {
 | |
|         expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
 | |
|         expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
 | |
|           expect.objectContaining({
 | |
|             size: wrapper.vm.csvText.length,
 | |
|             type: 'text/plain',
 | |
|           }),
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('when using dynamic modules', () => {
 | |
|     const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       store = createEmbedGroupStore();
 | |
|       store.registerModule(mockNamespace, monitoringDashboard);
 | |
|       store.state.embedGroup.modules.push(mockNamespace);
 | |
| 
 | |
|       wrapper = shallowMount(DashboardPanel, {
 | |
|         propsData: {
 | |
|           graphData,
 | |
|           settingsPath: dashboardProps.settingsPath,
 | |
|           namespace: mockNamespace,
 | |
|         },
 | |
|         store,
 | |
|         mocks,
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('handles namespaced time range and logs path state', () => {
 | |
|       store.state[mockNamespace].timeRange = mockTimeRange;
 | |
|       store.state[mockNamespace].logsPath = mockLogsPath;
 | |
| 
 | |
|       return wrapper.vm.$nextTick().then(() => {
 | |
|         expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('handles namespaced deployment data state', () => {
 | |
|       store.state[mockNamespace].deploymentData = mockDeploymentData;
 | |
| 
 | |
|       return wrapper.vm.$nextTick().then(() => {
 | |
|         expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('handles namespaced project path state', () => {
 | |
|       store.state[mockNamespace].projectPath = mockProjectPath;
 | |
| 
 | |
|       return wrapper.vm.$nextTick().then(() => {
 | |
|         expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('it renders a time series chart with no errors', () => {
 | |
|       expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
 | |
|       expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('panel timezone', () => {
 | |
|     it('displays a time chart in local timezone', () => {
 | |
|       createWrapper();
 | |
|       expect(findTimeChart().props('timezone')).toBe('LOCAL');
 | |
|     });
 | |
| 
 | |
|     it('displays a heatmap in local timezone', () => {
 | |
|       createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
 | |
|       expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
 | |
|     });
 | |
| 
 | |
|     describe('when timezone is set to UTC', () => {
 | |
|       beforeEach(() => {
 | |
|         store = createStore({ dashboardTimezone: 'UTC' });
 | |
|       });
 | |
| 
 | |
|       it('displays a time chart with UTC', () => {
 | |
|         createWrapper();
 | |
|         expect(findTimeChart().props('timezone')).toBe('UTC');
 | |
|       });
 | |
| 
 | |
|       it('displays a heatmap with UTC', () => {
 | |
|         createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
 | |
|         expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC');
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('Expand to full screen', () => {
 | |
|     const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
 | |
| 
 | |
|     describe('when there is no @expand listener', () => {
 | |
|       it('does not show `View full screen` option', () => {
 | |
|         createWrapper();
 | |
|         expect(findExpandBtn().exists()).toBe(false);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('when there is an @expand listener', () => {
 | |
|       beforeEach(() => {
 | |
|         createWrapper({}, { listeners: { expand: () => {} } });
 | |
|       });
 | |
| 
 | |
|       it('shows the `expand` option', () => {
 | |
|         expect(findExpandBtn().exists()).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('emits the `expand` event', () => {
 | |
|         const preventDefault = jest.fn();
 | |
|         findExpandBtn().vm.$emit('click', { preventDefault });
 | |
|         expect(wrapper.emitted('expand')).toHaveLength(1);
 | |
|         expect(preventDefault).toHaveBeenCalled();
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('panel alerts', () => {
 | |
|     const setMetricsSavedToDb = val =>
 | |
|       monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
 | |
|     const findAlertsWidget = () => wrapper.find(AlertWidget);
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       mockGetterReturnValue('metricsSavedToDb', []);
 | |
| 
 | |
|       createWrapper();
 | |
|     });
 | |
| 
 | |
|     describe.each`
 | |
|       desc                                              | metricsSavedToDb                   | props                                   | isShown
 | |
|       ${'with permission and no metrics in db'}         | ${[]}                              | ${{}}                                   | ${false}
 | |
|       ${'with permission and related metrics in db'}    | ${[graphData.metrics[0].metricId]} | ${{}}                                   | ${true}
 | |
|       ${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
 | |
|       ${'with permission and unrelated metrics in db'}  | ${['another_metric_id']}           | ${{}}                                   | ${false}
 | |
|     `('$desc', ({ metricsSavedToDb, isShown, props }) => {
 | |
|       const showsDesc = isShown ? 'shows' : 'does not show';
 | |
| 
 | |
|       beforeEach(() => {
 | |
|         setMetricsSavedToDb(metricsSavedToDb);
 | |
|         createWrapper({
 | |
|           alertsEndpoint: '/endpoint',
 | |
|           prometheusAlertsAvailable: true,
 | |
|           ...props,
 | |
|         });
 | |
|         return wrapper.vm.$nextTick();
 | |
|       });
 | |
| 
 | |
|       it(`${showsDesc} alert widget`, () => {
 | |
|         expect(findAlertsWidget().exists()).toBe(isShown);
 | |
|       });
 | |
| 
 | |
|       it(`${showsDesc} alert configuration`, () => {
 | |
|         expect(findMenuItemByText('Alerts').exists()).toBe(isShown);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('When graphData contains links', () => {
 | |
|     const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' });
 | |
|     const mockLinks = [
 | |
|       {
 | |
|         url: 'https://example.com',
 | |
|         title: 'Example 1',
 | |
|       },
 | |
|       {
 | |
|         url: 'https://gitlab.com',
 | |
|         title: 'Example 2',
 | |
|       },
 | |
|     ];
 | |
|     const createWrapperWithLinks = (links = mockLinks) => {
 | |
|       createWrapper({
 | |
|         graphData: {
 | |
|           ...graphData,
 | |
|           links,
 | |
|         },
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     it('custom links are shown', () => {
 | |
|       createWrapperWithLinks();
 | |
| 
 | |
|       mockLinks.forEach(({ url, title }) => {
 | |
|         const link = findMenuItemByText(title).at(0);
 | |
| 
 | |
|         expect(link.exists()).toBe(true);
 | |
|         expect(link.attributes('href')).toBe(url);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it("custom links don't show unsecure content", () => {
 | |
|       createWrapperWithLinks([
 | |
|         {
 | |
|           title: '<script>alert("XSS")</script>',
 | |
|           url: 'http://example.com',
 | |
|         },
 | |
|       ]);
 | |
| 
 | |
|       expect(findMenuItems().at(1).element.innerHTML).toBe(
 | |
|         '<script>alert("XSS")</script>',
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it("custom links don't show unsecure href attributes", () => {
 | |
|       const title = 'Owned!';
 | |
| 
 | |
|       createWrapperWithLinks([
 | |
|         {
 | |
|           title,
 | |
|           // eslint-disable-next-line no-script-url
 | |
|           url: 'javascript:alert("Evil")',
 | |
|         },
 | |
|       ]);
 | |
| 
 | |
|       const link = findMenuItemByText(title).at(0);
 | |
|       expect(link.attributes('href')).toBe('#');
 | |
|     });
 | |
| 
 | |
|     it('when an editable dashboard is selected, shows `Manage chart links` link to the blob path', () => {
 | |
|       const editUrl = '/edit';
 | |
|       mockGetterReturnValue('selectedDashboard', {
 | |
|         can_edit: true,
 | |
|         project_blob_path: editUrl,
 | |
|       });
 | |
|       createWrapperWithLinks();
 | |
| 
 | |
|       expect(findManageLinksItem().exists()).toBe(true);
 | |
|       expect(findManageLinksItem().attributes('href')).toBe(editUrl);
 | |
|     });
 | |
| 
 | |
|     it('when no dashboard is selected, does not show `Manage chart links`', () => {
 | |
|       mockGetterReturnValue('selectedDashboard', null);
 | |
|       createWrapperWithLinks();
 | |
| 
 | |
|       expect(findManageLinksItem().exists()).toBe(false);
 | |
|     });
 | |
| 
 | |
|     it('when non-editable dashboard is selected, does not show `Manage chart links`', () => {
 | |
|       const editUrl = '/edit';
 | |
|       mockGetterReturnValue('selectedDashboard', {
 | |
|         can_edit: false,
 | |
|         project_blob_path: editUrl,
 | |
|       });
 | |
|       createWrapperWithLinks();
 | |
| 
 | |
|       expect(findManageLinksItem().exists()).toBe(false);
 | |
|     });
 | |
|   });
 | |
| });
 |