diff options
Diffstat (limited to 'spec/frontend/monitoring/components')
19 files changed, 1738 insertions, 848 deletions
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index e7c51d82cd2..7ef956f8e05 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -20,7 +20,6 @@ exports[`Dashboard template matches the default snapshot 1`] = ` data-qa-selector="dashboards_filter_dropdown" defaultbranch="master" id="monitor-dashboards-dropdown" - modalid="duplicateDashboard" toggle-class="dropdown-menu-toggle" /> </div> @@ -33,26 +32,24 @@ exports[`Dashboard template matches the default snapshot 1`] = ` <div class="mb-2 pr-2 d-flex d-sm-block" > - <gl-dropdown-stub + <gl-new-dropdown-stub + category="tertiary" class="flex-grow-1" data-qa-selector="environments_dropdown" + headertext="" id="monitor-environments-dropdown" menu-class="monitor-environment-dropdown-menu" + size="medium" text="production" - toggle-class="dropdown-menu-toggle" + toggleclass="dropdown-menu-toggle" + variant="default" > <div class="d-flex flex-column overflow-hidden" > - <gl-dropdown-header-stub - class="monitor-environment-dropdown-header text-center" - > - - Environment - - </gl-dropdown-header-stub> - - <gl-dropdown-divider-stub /> + <gl-new-dropdown-header-stub> + Environment + </gl-new-dropdown-header-stub> <gl-search-box-by-type-stub class="m-2" @@ -72,7 +69,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` </div> </div> - </gl-dropdown-stub> + </gl-new-dropdown-stub> </div> <div @@ -100,45 +97,23 @@ exports[`Dashboard template matches the default snapshot 1`] = ` <div class="d-sm-flex" > - <div - class="mb-2 mr-2 d-flex" - > - <div - class="flex-grow-1" - title="Star dashboard" - > - <gl-deprecated-button-stub - class="w-100" - size="md" - variant="default" - > - <gl-icon-stub - name="star-o" - size="16" - /> - </gl-deprecated-button-stub> - </div> - </div> - <!----> <!----> - <!----> - - <!----> - - <!----> - - <!----> + <div + class="gl-mb-3 gl-mr-3 d-flex d-sm-block" + > + <actions-menu-stub + custommetricspath="/monitoring/monitor-project/prometheus/metrics" + defaultbranch="master" + isootbdashboard="true" + validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query" + /> + </div> <!----> </div> - - <duplicate-dashboard-modal-stub - defaultbranch="master" - modalid="duplicateDashboard" - /> </div> <empty-state-stub diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js index a8416216a94..6d71a9b09e5 100644 --- a/spec/frontend/monitoring/components/alert_widget_form_spec.js +++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlLink } from '@gitlab/ui'; +import INVALID_URL from '~/lib/utils/invalid_url'; import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue'; import ModalStub from '../stubs/modal_stub'; @@ -24,7 +25,13 @@ describe('AlertWidgetForm', () => { const propsWithAlertData = { ...defaultProps, alertsToManage: { - alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId }, + alert: { + alert_path: alertPath, + operator: '<', + threshold: 5, + metricId, + runbookUrl: INVALID_URL, + }, }, configuredAlert: metricId, }; @@ -46,15 +53,11 @@ describe('AlertWidgetForm', () => { const modal = () => wrapper.find(ModalStub); const modalTitle = () => modal().attributes('title'); const submitButton = () => modal().find(GlLink); + const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]'); + const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]'); const submitButtonTrackingOpts = () => JSON.parse(submitButton().attributes('data-tracking-options')); - const e = { - preventDefault: jest.fn(), - }; - - beforeEach(() => { - e.preventDefault.mockReset(); - }); + const stubEvent = { preventDefault: jest.fn() }; afterEach(() => { if (wrapper) wrapper.destroy(); @@ -81,35 +84,34 @@ describe('AlertWidgetForm', () => { expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create); }); - it('emits a "create" event when form submitted without existing alert', () => { - createComponent(); + it('emits a "create" event when form submitted without existing alert', async () => { + createComponent(defaultProps); - wrapper.vm.selectQuery('9'); - wrapper.setData({ - threshold: 900, - }); + modal().vm.$emit('shown'); + + findThresholdField().vm.$emit('input', 900); + findRunbookField().vm.$emit('input', INVALID_URL); - wrapper.vm.handleSubmit(e); + modal().vm.$emit('ok', stubEvent); expect(wrapper.emitted().create[0]).toEqual([ { alert: undefined, operator: '>', threshold: 900, - prometheus_metric_id: '9', + prometheus_metric_id: '8', + runbookUrl: INVALID_URL, }, ]); - expect(e.preventDefault).toHaveBeenCalledTimes(1); }); it('resets form when modal is dismissed (hidden)', () => { - createComponent(); + createComponent(defaultProps); - wrapper.vm.selectQuery('9'); - wrapper.vm.selectQuery('>'); - wrapper.setData({ - threshold: 800, - }); + modal().vm.$emit('shown'); + + findThresholdField().vm.$emit('input', 800); + findRunbookField().vm.$emit('input', INVALID_URL); modal().vm.$emit('hidden'); @@ -117,6 +119,7 @@ describe('AlertWidgetForm', () => { expect(wrapper.vm.operator).toBe(null); expect(wrapper.vm.threshold).toBe(null); expect(wrapper.vm.prometheusMetricId).toBe(null); + expect(wrapper.vm.runbookUrl).toBe(null); }); it('sets selectedAlert to the provided configuredAlert on modal show', () => { @@ -163,7 +166,7 @@ describe('AlertWidgetForm', () => { beforeEach(() => { createComponent(propsWithAlertData); - wrapper.vm.selectQuery(metricId); + modal().vm.$emit('shown'); }); it('sets tracking options for delete alert', () => { @@ -176,7 +179,7 @@ describe('AlertWidgetForm', () => { }); it('emits "delete" event when form values unchanged', () => { - wrapper.vm.handleSubmit(e); + modal().vm.$emit('ok', stubEvent); expect(wrapper.emitted().delete[0]).toEqual([ { @@ -184,37 +187,52 @@ describe('AlertWidgetForm', () => { operator: '<', threshold: 5, prometheus_metric_id: '8', + runbookUrl: INVALID_URL, }, ]); - expect(e.preventDefault).toHaveBeenCalledTimes(1); }); + }); - it('emits "update" event when form changed', () => { - wrapper.setData({ - threshold: 11, - }); + it('emits "update" event when form changed', () => { + const updatedRunbookUrl = `${INVALID_URL}/test`; - wrapper.vm.handleSubmit(e); + createComponent(propsWithAlertData); - expect(wrapper.emitted().update[0]).toEqual([ - { - alert: 'alert', - operator: '<', - threshold: 11, - prometheus_metric_id: '8', - }, - ]); - expect(e.preventDefault).toHaveBeenCalledTimes(1); - }); + modal().vm.$emit('shown'); + + findRunbookField().vm.$emit('input', updatedRunbookUrl); + findThresholdField().vm.$emit('input', 11); - it('sets tracking options for update alert', () => { - wrapper.setData({ + modal().vm.$emit('ok', stubEvent); + + expect(wrapper.emitted().update[0]).toEqual([ + { + alert: 'alert', + operator: '<', threshold: 11, - }); + prometheus_metric_id: '8', + runbookUrl: updatedRunbookUrl, + }, + ]); + }); + + it('sets tracking options for update alert', async () => { + createComponent(propsWithAlertData); + + modal().vm.$emit('shown'); + + findThresholdField().vm.$emit('input', 11); + + await wrapper.vm.$nextTick(); + + expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update); + }); + + describe('alert runbooks', () => { + it('shows the runbook field', () => { + createComponent(); - return wrapper.vm.$nextTick(() => { - expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update); - }); + expect(findRunbookField().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js new file mode 100644 index 00000000000..850e2ca87db --- /dev/null +++ b/spec/frontend/monitoring/components/charts/gauge_spec.js @@ -0,0 +1,215 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlGaugeChart } from '@gitlab/ui/dist/charts'; +import GaugeChart from '~/monitoring/components/charts/gauge.vue'; +import { gaugeChartGraphData } from '../../graph_data'; + +describe('Gauge Chart component', () => { + const defaultGraphData = gaugeChartGraphData(); + + let wrapper; + + const findGaugeChart = () => wrapper.find(GlGaugeChart); + + const createWrapper = ({ ...graphProps } = {}) => { + wrapper = shallowMount(GaugeChart, { + propsData: { + graphData: { + ...defaultGraphData, + ...graphProps, + }, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('chart component', () => { + it('is rendered when props are passed', () => { + createWrapper(); + + expect(findGaugeChart().exists()).toBe(true); + }); + }); + + describe('min and max', () => { + const MIN_DEFAULT = 0; + const MAX_DEFAULT = 100; + + it('are passed to chart component', () => { + createWrapper(); + + expect(findGaugeChart().props('min')).toBe(100); + expect(findGaugeChart().props('max')).toBe(1000); + }); + + const invalidCases = [undefined, NaN, 'a string']; + + it.each(invalidCases)( + 'if min has invalid value, defaults are used for both min and max', + invalidValue => { + createWrapper({ minValue: invalidValue }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }, + ); + + it.each(invalidCases)( + 'if max has invalid value, defaults are used for both min and max', + invalidValue => { + createWrapper({ minValue: invalidValue }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }, + ); + + it('if min is bigger than max, defaults are used for both min and max', () => { + createWrapper({ minValue: 100, maxValue: 0 }); + + expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT); + expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT); + }); + }); + + describe('thresholds', () => { + it('thresholds are set on chart', () => { + createWrapper(); + + expect(findGaugeChart().props('thresholds')).toEqual([500, 800]); + }); + + it('when no thresholds are defined, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + minValue: 0, + maxValue: 100, + thresholds: {}, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([95]); + }); + + it('when out of min-max bounds thresholds are defined, a default threshold is defined at 95% of the range between min_value and max_value', () => { + createWrapper({ + thresholds: { + values: [-10, 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + + describe('when mode is absolute', () => { + it('only valid threshold values are used', () => { + createWrapper({ + thresholds: { + mode: 'absolute', + values: [undefined, 10, 110, NaN, 'a string', 400], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([110, 400]); + }); + + it('if all threshold values are invalid, a default threshold is defined at 95% of the range between min_value and max_value', () => { + createWrapper({ + thresholds: { + mode: 'absolute', + values: [NaN, undefined, 'a string', 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + }); + + describe('when mode is percentage', () => { + it('when values outside of 0-100 bounds are used, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + thresholds: { + mode: 'percentage', + values: [110], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + + it('if all threshold values are invalid, a default threshold is defined at 95% of max_value', () => { + createWrapper({ + thresholds: { + mode: 'percentage', + values: [NaN, undefined, 'a string', 1500], + }, + }); + + expect(findGaugeChart().props('thresholds')).toEqual([855]); + }); + }); + }); + + describe('split (the number of ticks on the chart arc)', () => { + const SPLIT_DEFAULT = 10; + + it('is passed to chart as prop', () => { + createWrapper(); + + expect(findGaugeChart().props('splitNumber')).toBe(20); + }); + + it('if not explicitly set, passes a default value to chart', () => { + createWrapper({ split: '' }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + + it('if set as a number that is not an integer, passes the default value to chart', () => { + createWrapper({ split: 10.5 }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + + it('if set as a negative number, passes the default value to chart', () => { + createWrapper({ split: -10 }); + + expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT); + }); + }); + + describe('text (the text displayed on the gauge for the current value)', () => { + it('displays the query result value when format is not set', () => { + createWrapper({ format: '' }); + + expect(findGaugeChart().props('text')).toBe('3'); + }); + + it('displays the query result value when format is set to invalid value', () => { + createWrapper({ format: 'invalid' }); + + expect(findGaugeChart().props('text')).toBe('3'); + }); + + it('displays a formatted query result value when format is set', () => { + createWrapper(); + + expect(findGaugeChart().props('text')).toBe('3kB'); + }); + + it('displays a placeholder value when metric is empty', () => { + createWrapper({ metrics: [] }); + + expect(findGaugeChart().props('text')).toBe('--'); + }); + }); + + describe('value', () => { + it('correct value is passed', () => { + createWrapper(); + + expect(findGaugeChart().props('value')).toBe(3); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js index 2a1c78025ae..27a2021e9be 100644 --- a/spec/frontend/monitoring/components/charts/heatmap_spec.js +++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlHeatmap } from '@gitlab/ui/dist/charts'; import timezoneMock from 'timezone-mock'; import Heatmap from '~/monitoring/components/charts/heatmap.vue'; -import { graphDataPrometheusQueryRangeMultiTrack } from '../../mock_data'; +import { heatmapGraphData } from '../../graph_data'; describe('Heatmap component', () => { let wrapper; @@ -10,10 +10,12 @@ describe('Heatmap component', () => { const findChart = () => wrapper.find(GlHeatmap); + const graphData = heatmapGraphData(); + const createWrapper = (props = {}) => { wrapper = shallowMount(Heatmap, { propsData: { - graphData: graphDataPrometheusQueryRangeMultiTrack, + graphData: heatmapGraphData(), containerWidth: 100, ...props, }, @@ -38,11 +40,11 @@ describe('Heatmap component', () => { }); it('should display a label on the x axis', () => { - expect(wrapper.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label); + expect(wrapper.vm.xAxisName).toBe(graphData.xLabel); }); it('should display a label on the y axis', () => { - expect(wrapper.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label); + expect(wrapper.vm.yAxisName).toBe(graphData.y_label); }); // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data @@ -54,24 +56,24 @@ describe('Heatmap component', () => { const row = wrapper.vm.chartData[0]; expect(row.length).toBe(3); - expect(wrapper.vm.chartData.length).toBe(30); + expect(wrapper.vm.chartData.length).toBe(6); }); it('returns a series of labels for the x axis', () => { const { xAxisLabels } = wrapper.vm; - expect(xAxisLabels.length).toBe(5); + expect(xAxisLabels.length).toBe(2); }); describe('y axis labels', () => { - const gmtLabels = ['3:00 PM', '4:00 PM', '5:00 PM', '6:00 PM', '7:00 PM', '8:00 PM']; + const gmtLabels = ['8:10 PM', '8:12 PM', '8:14 PM']; it('y-axis labels are formatted in AM/PM format', () => { expect(findChart().props('yAxisLabels')).toEqual(gmtLabels); }); describe('when in PT timezone', () => { - const ptLabels = ['8:00 AM', '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', '1:00 PM']; + const ptLabels = ['1:10 PM', '1:12 PM', '1:14 PM']; const utcLabels = gmtLabels; // Identical in this case beforeAll(() => { diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js index 1c8fdc01e3e..3372d27e4f9 100644 --- a/spec/frontend/monitoring/components/charts/options_spec.js +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -1,5 +1,9 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; -import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options'; +import { + getYAxisOptions, + getTooltipFormatter, + getValidThresholds, +} from '~/monitoring/components/charts/options'; describe('options spec', () => { describe('getYAxisOptions', () => { @@ -82,4 +86,242 @@ describe('options spec', () => { expect(formatter(1)).toBe('1.000B'); }); }); + + describe('getValidThresholds', () => { + const invalidCases = [null, undefined, NaN, 'a string', true, false]; + + let thresholds; + + afterEach(() => { + thresholds = null; + }); + + it('returns same thresholds when passed values within range', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([10, 50]); + }); + + it('filters out thresholds that are out of range', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [-5, 10, 110], + }); + + expect(thresholds).toEqual([10]); + }); + it('filters out duplicate thresholds', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [5, 5, 10, 10], + }); + + expect(thresholds).toEqual([5, 10]); + }); + + it('sorts passed thresholds and applies only the first two in ascending order', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 1, 35, 20, 5], + }); + + expect(thresholds).toEqual([1, 5]); + }); + + it('thresholds equal to min or max are filtered out', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [0, 100], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)('invalid values for thresholds are filtered out', invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, invalidValue], + }); + + expect(thresholds).toEqual([10]); + }); + + describe('range', () => { + it('when range is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when min is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { max: 100 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when max is not defined, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it('when min is larger than max, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 100, max: 0 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)( + 'when min has invalid value, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: invalidValue, max: 100 }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }, + ); + + it.each(invalidCases)( + 'when max has invalid value, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: invalidValue }, + values: [10, 20], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + + describe('values', () => { + it('if values parameter is omitted, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + }); + + expect(thresholds).toEqual([]); + }); + + it('if there are no values passed, empty result is returned', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [], + }); + + expect(thresholds).toEqual([]); + }); + + it.each(invalidCases)( + 'if invalid values are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [invalidValue], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + + describe('mode', () => { + it.each(invalidCases)( + 'if invalid values are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: invalidValue, + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([]); + }, + ); + + it('if mode is not passed, empty result is returned', () => { + thresholds = getValidThresholds({ + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([]); + }); + + describe('absolute mode', () => { + it('absolute mode behaves correctly', () => { + thresholds = getValidThresholds({ + mode: 'absolute', + range: { min: 0, max: 100 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([10, 50]); + }); + }); + + describe('percentage mode', () => { + it('percentage mode behaves correctly', () => { + thresholds = getValidThresholds({ + mode: 'percentage', + range: { min: 0, max: 1000 }, + values: [10, 50], + }); + + expect(thresholds).toEqual([100, 500]); + }); + + const outOfPercentBoundsValues = [-1, 0, 100, 101]; + it.each(outOfPercentBoundsValues)( + 'when values out of 0-100 range are passed, empty result is returned', + invalidValue => { + thresholds = getValidThresholds({ + mode: 'percentage', + range: { min: 0, max: 1000 }, + values: [invalidValue], + }); + + expect(thresholds).toEqual([]); + }, + ); + }); + }); + + it('calling without passing object parameter returns empty array', () => { + thresholds = getValidThresholds(); + + expect(thresholds).toEqual([]); + }); + }); }); diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js index 3783b1eebd2..37712eb3012 100644 --- a/spec/frontend/monitoring/components/charts/single_stat_spec.js +++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js @@ -1,71 +1,91 @@ import { shallowMount } from '@vue/test-utils'; +import { GlSingleStat } from '@gitlab/ui/dist/charts'; import SingleStatChart from '~/monitoring/components/charts/single_stat.vue'; import { singleStatGraphData } from '../../graph_data'; describe('Single Stat Chart component', () => { - let singleStatChart; + let wrapper; - beforeEach(() => { - singleStatChart = shallowMount(SingleStatChart, { + const createComponent = (props = {}) => { + wrapper = shallowMount(SingleStatChart, { propsData: { graphData: singleStatGraphData({}, { unit: 'MB' }), + ...props, }, }); + }; + + const findChart = () => wrapper.find(GlSingleStat); + + beforeEach(() => { + createComponent(); }); afterEach(() => { - singleStatChart.destroy(); + wrapper.destroy(); }); describe('computed', () => { describe('statValue', () => { it('should interpolate the value and unit props', () => { - expect(singleStatChart.vm.statValue).toBe('1.00MB'); + expect(findChart().props('value')).toBe('1.00MB'); }); it('should change the value representation to a percentile one', () => { - singleStatChart.setProps({ + createComponent({ graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }), }); - expect(singleStatChart.vm.statValue).toContain('75.83%'); + expect(findChart().props('value')).toContain('75.83%'); }); it('should display NaN for non numeric maxValue values', () => { - singleStatChart.setProps({ + createComponent({ graphData: singleStatGraphData({ max_value: 'not a number' }), }); - expect(singleStatChart.vm.statValue).toContain('NaN'); + expect(findChart().props('value')).toContain('NaN'); }); it('should display NaN for missing query values', () => { - singleStatChart.setProps({ + createComponent({ graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }), }); - expect(singleStatChart.vm.statValue).toContain('NaN'); + expect(findChart().props('value')).toContain('NaN'); + }); + + it('should not display `unit` when `unit` is undefined', () => { + createComponent({ + graphData: singleStatGraphData({}, { unit: undefined }), + }); + + expect(findChart().props('value')).not.toContain('undefined'); }); - describe('field attribute', () => { + it('should not display `unit` when `unit` is null', () => { + createComponent({ + graphData: singleStatGraphData({}, { unit: null }), + }); + + expect(findChart().props('value')).not.toContain('null'); + }); + + describe('when a field attribute is set', () => { it('displays a label value instead of metric value when field attribute is used', () => { - singleStatChart.setProps({ + createComponent({ graphData: singleStatGraphData({ field: 'job' }, { isVector: true }), }); - return singleStatChart.vm.$nextTick(() => { - expect(singleStatChart.vm.statValue).toContain('prometheus'); - }); + expect(findChart().props('value')).toContain('prometheus'); }); it('displays No data to display if field attribute is not present', () => { - singleStatChart.setProps({ + createComponent({ graphData: singleStatGraphData({ field: 'this-does-not-exist' }), }); - return singleStatChart.vm.$nextTick(() => { - expect(singleStatChart.vm.statValue).toContain('No data to display'); - }); + expect(findChart().props('value')).toContain('No data to display'); }); }); }); diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 97386be9e32..6f9a89feb3e 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -12,7 +12,12 @@ import { import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import { panelTypes, chartHeight } from '~/monitoring/constants'; import TimeSeries from '~/monitoring/components/charts/time_series.vue'; -import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data'; +import { + deploymentData, + mockProjectDir, + annotationsData, + mockFixedTimeRange, +} from '../../mock_data'; import { timeSeriesGraphData } from '../../graph_data'; @@ -42,6 +47,7 @@ describe('Time series component', () => { deploymentData, annotations: annotationsData, projectPath: `${TEST_HOST}${mockProjectDir}`, + timeRange: mockFixedTimeRange, ...props, }, stubs: { @@ -382,6 +388,25 @@ describe('Time series component', () => { }); describe('chartOptions', () => { + describe('x-Axis bounds', () => { + it('is set to the time range bounds', () => { + expect(getChartOptions().xAxis).toMatchObject({ + min: mockFixedTimeRange.start, + max: mockFixedTimeRange.end, + }); + }); + + it('is not set if time range is not set or incorrectly set', () => { + wrapper.setProps({ + timeRange: {}, + }); + return wrapper.vm.$nextTick(() => { + expect(getChartOptions().xAxis).not.toHaveProperty('min'); + expect(getChartOptions().xAxis).not.toHaveProperty('max'); + }); + }); + }); + describe('dataZoom', () => { it('renders with scroll handle icons', () => { expect(getChartOptions().dataZoom).toHaveLength(1); diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js new file mode 100644 index 00000000000..024b2cbd7f1 --- /dev/null +++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js @@ -0,0 +1,440 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlNewDropdownItem } from '@gitlab/ui'; +import { createStore } from '~/monitoring/stores'; +import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants'; +import { setupAllDashboards, setupStoreWithData } from '../store_utils'; +import { redirectTo } from '~/lib/utils/url_utility'; +import Tracking from '~/tracking'; +import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue'; +import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; +import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data'; +import * as types from '~/monitoring/stores/mutation_types'; + +jest.mock('~/lib/utils/url_utility', () => ({ + redirectTo: jest.fn(), + queryToObject: jest.fn(), +})); + +describe('Actions menu', () => { + const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]]; + const customDashboard = dashboardGitResponse[1]; + + let store; + let wrapper; + + const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]'); + const findAddPanelItemEnabled = () => wrapper.find('[data-testid="add-panel-item-enabled"]'); + const findAddPanelItemDisabled = () => wrapper.find('[data-testid="add-panel-item-disabled"]'); + const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]'); + const findAddMetricModalSubmitButton = () => + wrapper.find('[data-testid="add-metric-modal-submit-button"]'); + const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]'); + const findEditDashboardItemEnabled = () => + wrapper.find('[data-testid="edit-dashboard-item-enabled"]'); + const findEditDashboardItemDisabled = () => + wrapper.find('[data-testid="edit-dashboard-item-disabled"]'); + const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]'); + const findDuplicateDashboardModal = () => + wrapper.find('[data-testid="duplicate-dashboard-modal"]'); + const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]'); + const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]'); + + const createShallowWrapper = (props = {}, options = {}) => { + wrapper = shallowMount(ActionsMenu, { + propsData: { ...dashboardActionsMenuProps, ...props }, + store, + ...options, + }); + }; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('add metric item', () => { + it('is rendered when custom metrics are available', () => { + createShallowWrapper(); + + return wrapper.vm.$nextTick(() => { + expect(findAddMetricItem().exists()).toBe(true); + }); + }); + + it('is not rendered when custom metrics are not available', () => { + createShallowWrapper({ + addingMetricsAvailable: false, + }); + + return wrapper.vm.$nextTick(() => { + expect(findAddMetricItem().exists()).toBe(false); + }); + }); + + describe('when available', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + it('modal for custom metrics form is rendered', () => { + expect(findAddMetricModal().exists()).toBe(true); + expect(findAddMetricModal().attributes().modalid).toBe('addMetric'); + }); + + it('add metric modal submit button exists', () => { + expect(findAddMetricModalSubmitButton().exists()).toBe(true); + }); + + it('renders custom metrics form fields', () => { + expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true); + }); + }); + + describe('when not available', () => { + beforeEach(() => { + createShallowWrapper({ addingMetricsAvailable: false }); + }); + + it('modal for custom metrics form is not rendered', () => { + expect(findAddMetricModal().exists()).toBe(false); + }); + }); + + describe('adding new metric from modal', () => { + let origPage; + + beforeEach(done => { + jest.spyOn(Tracking, 'event').mockReturnValue(); + createShallowWrapper(); + + setupStoreWithData(store); + + origPage = document.body.dataset.page; + document.body.dataset.page = 'projects:environments:metrics'; + + wrapper.vm.$nextTick(done); + }); + + afterEach(() => { + document.body.dataset.page = origPage; + }); + + it('is tracked', done => { + const submitButton = findAddMetricModalSubmitButton().vm; + + wrapper.vm.$nextTick(() => { + submitButton.$el.click(); + wrapper.vm.$nextTick(() => { + expect(Tracking.event).toHaveBeenCalledWith( + document.body.dataset.page, + 'click_button', + { + label: 'add_new_metric', + property: 'modal', + value: undefined, + }, + ); + done(); + }); + }); + }); + }); + }); + + describe('add panel item', () => { + const GlNewDropdownItemStub = { + extends: GlNewDropdownItem, + props: { + to: [String, Object], + }, + }; + + let $route; + + beforeEach(() => { + $route = { name: DASHBOARD_PAGE, params: { dashboard: 'my_dashboard.yml' } }; + + createShallowWrapper( + { + isOotbDashboard: false, + }, + { + mocks: { $route }, + stubs: { GlNewDropdownItem: GlNewDropdownItemStub }, + }, + ); + }); + + it('is disabled for ootb dashboards', () => { + createShallowWrapper({ + isOotbDashboard: true, + }); + + return wrapper.vm.$nextTick(() => { + expect(findAddPanelItemDisabled().exists()).toBe(true); + }); + }); + + it('is visible for custom dashboards', () => { + expect(findAddPanelItemEnabled().exists()).toBe(true); + }); + + it('renders a link to the new panel page for custom dashboards', () => { + expect(findAddPanelItemEnabled().props('to')).toEqual({ + name: PANEL_NEW_PAGE, + params: { + dashboard: 'my_dashboard.yml', + }, + }); + }); + }); + + describe('edit dashboard yml item', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + describe('when current dashboard is custom', () => { + beforeEach(() => { + setupAllDashboards(store, customDashboard.path); + }); + + it('enabled item is rendered and has falsy disabled attribute', () => { + expect(findEditDashboardItemEnabled().exists()).toBe(true); + expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined); + }); + + it('enabled item links to their edit path', () => { + expect(findEditDashboardItemEnabled().attributes('href')).toBe( + customDashboard.project_blob_path, + ); + }); + + it('disabled item is not rendered', () => { + expect(findEditDashboardItemDisabled().exists()).toBe(false); + }); + }); + + describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => { + beforeEach(() => { + setupAllDashboards(store, dashboard.path); + }); + + it('disabled item is rendered and has disabled attribute set on it', () => { + expect(findEditDashboardItemDisabled().exists()).toBe(true); + expect(findEditDashboardItemDisabled().attributes('disabled')).toBe(''); + }); + + it('enabled item is not rendered', () => { + expect(findEditDashboardItemEnabled().exists()).toBe(false); + }); + }); + }); + + describe('duplicate dashboard item', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => { + beforeEach(() => { + setupAllDashboards(store, dashboard.path); + }); + + it('is rendered', () => { + expect(findDuplicateDashboardItem().exists()).toBe(true); + }); + + it('duplicate dashboard modal is rendered', () => { + expect(findDuplicateDashboardModal().exists()).toBe(true); + }); + + it('clicking on item opens up the duplicate dashboard modal', () => { + const modalId = 'duplicateDashboard'; + const modalTrigger = findDuplicateDashboardItem(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + modalTrigger.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + }); + }); + }); + + describe('when current dashboard is custom', () => { + beforeEach(() => { + setupAllDashboards(store, customDashboard.path); + }); + + it('is not rendered', () => { + expect(findDuplicateDashboardItem().exists()).toBe(false); + }); + + it('duplicate dashboard modal is not rendered', () => { + expect(findDuplicateDashboardModal().exists()).toBe(false); + }); + }); + + describe('when no dashboard is set', () => { + it('is not rendered', () => { + expect(findDuplicateDashboardItem().exists()).toBe(false); + }); + + it('duplicate dashboard modal is not rendered', () => { + expect(findDuplicateDashboardModal().exists()).toBe(false); + }); + }); + + describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => { + beforeEach(() => { + store.state.monitoringDashboard.projectPath = 'root/sandbox'; + + setupAllDashboards(store, dashboardGitResponse[0].path); + }); + + it('redirects to the newly created dashboard', () => { + delete window.location; + window.location = new URL('https://localhost'); + + const newDashboard = dashboardGitResponse[1]; + + const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml'; + findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard); + + return wrapper.vm.$nextTick().then(() => { + expect(redirectTo).toHaveBeenCalled(); + expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); + }); + }); + }); + }); + + describe('star dashboard item', () => { + beforeEach(() => { + createShallowWrapper(); + setupAllDashboards(store); + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + }); + + it('is shown', () => { + expect(findStarDashboardItem().exists()).toBe(true); + }); + + it('is not disabled', () => { + expect(findStarDashboardItem().attributes('disabled')).toBeFalsy(); + }); + + it('is disabled when starring is taking place', () => { + store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`); + + return wrapper.vm.$nextTick(() => { + expect(findStarDashboardItem().exists()).toBe(true); + expect(findStarDashboardItem().attributes('disabled')).toBe('true'); + }); + }); + + it('on click it dispatches a toggle star action', () => { + findStarDashboardItem().vm.$emit('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/toggleStarredValue', + undefined, + ); + }); + }); + + describe('when dashboard is not starred', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { + currentDashboard: dashboardGitResponse[0].path, + }); + return wrapper.vm.$nextTick(); + }); + + it('item text shows "Star dashboard"', () => { + expect(findStarDashboardItem().html()).toMatch(/Star dashboard/); + }); + }); + + describe('when dashboard is starred', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { + currentDashboard: dashboardGitResponse[1].path, + }); + return wrapper.vm.$nextTick(); + }); + + it('item text shows "Unstar dashboard"', () => { + expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/); + }); + }); + }); + + describe('create dashboard item', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + it('is rendered by default but it is disabled', () => { + expect(findCreateDashboardItem().attributes('disabled')).toBe('true'); + }); + + describe('when project path is set', () => { + const mockProjectPath = 'root/sandbox'; + const mockAddDashboardDocPath = '/doc/add-dashboard'; + + beforeEach(() => { + store.state.monitoringDashboard.projectPath = mockProjectPath; + store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath; + }); + + it('is not disabled', () => { + expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined); + }); + + it('renders a modal for creating a dashboard', () => { + expect(findCreateDashboardModal().exists()).toBe(true); + }); + + it('clicking opens up the modal', () => { + const modalId = 'createDashboard'; + const modalTrigger = findCreateDashboardItem(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + modalTrigger.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + }); + }); + + it('modal gets passed correct props', () => { + expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath); + expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe( + mockAddDashboardDocPath, + ); + }); + }); + + describe('when project path is not set', () => { + beforeEach(() => { + store.state.monitoringDashboard.projectPath = null; + }); + + it('is disabled', () => { + expect(findCreateDashboardItem().attributes('disabled')).toBe('true'); + }); + + it('does not render a modal for creating a dashboard', () => { + expect(findCreateDashboardModal().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js index 5a1a615c703..5cf24706ebd 100644 --- a/spec/frontend/monitoring/components/dashboard_header_spec.js +++ b/spec/frontend/monitoring/components/dashboard_header_spec.js @@ -1,16 +1,23 @@ import { shallowMount } from '@vue/test-utils'; +import { GlNewDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui'; import { createStore } from '~/monitoring/stores'; +import * as types from '~/monitoring/stores/mutation_types'; +import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; +import RefreshButton from '~/monitoring/components/refresh_button.vue'; import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; -import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue'; -import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue'; -import { setupAllDashboards } from '../store_utils'; +import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; +import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue'; +import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils'; import { + environmentData, dashboardGitResponse, selfMonitoringDashboardGitResponse, dashboardHeaderProps, } from '../mock_data'; import { redirectTo } from '~/lib/utils/url_utility'; +const mockProjectPath = 'https://path/to/project'; + jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), queryToObject: jest.fn(), @@ -21,13 +28,22 @@ describe('Dashboard header', () => { let store; let wrapper; - const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]'); - const findCreateDashboardMenuItem = () => - findActionsMenu().find('[data-testid="action-create-dashboard"]'); - const findCreateDashboardDuplicateItem = () => - findActionsMenu().find('[data-testid="action-duplicate-dashboard"]'); - const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal); - const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]'); + const findDashboardDropdown = () => wrapper.find(DashboardsDropdown); + + const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); + const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlNewDropdownItem); + const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType); + const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }); + const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon); + + const findDateTimePicker = () => wrapper.find(DateTimePicker); + const findRefreshButton = () => wrapper.find(RefreshButton); + + const findActionsMenu = () => wrapper.find(ActionsMenu); + + const setSearchTerm = searchTerm => { + store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); + }; const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(DashboardHeader, { @@ -45,139 +61,315 @@ describe('Dashboard header', () => { wrapper.destroy(); }); - describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => { + describe('dashboards dropdown', () => { beforeEach(() => { - store.state.monitoringDashboard.projectPath = 'root/sandbox'; + store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { + projectPath: mockProjectPath, + }); + + createShallowWrapper(); }); - /** - * The duplicate dashboard modal gets called both by a menu item from the - * dashboards dropdown and by an item from the actions menu. - * - * This spec is context agnostic, so it addresses all cases where the - * duplicate dashboard modal gets called. - */ - it('redirects to the newly created dashboard', () => { - delete window.location; - window.location = new URL('https://localhost'); - const newDashboard = dashboardGitResponse[1]; + it('shows the dashboard dropdown', () => { + expect(findDashboardDropdown().exists()).toBe(true); + }); - createShallowWrapper(); + it('when an out of the box dashboard is selected, encodes dashboard path', () => { + findDashboardDropdown().vm.$emit('selectDashboard', { + path: '.gitlab/dashboards/dashboard©.yml', + out_of_the_box_dashboard: true, + display_name: 'A display name', + }); - const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml'; - findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard); + expect(redirectTo).toHaveBeenCalledWith( + `${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`, + ); + }); - return wrapper.vm.$nextTick().then(() => { - expect(redirectTo).toHaveBeenCalled(); - expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); + it('when a custom dashboard is selected, encodes dashboard display name', () => { + findDashboardDropdown().vm.$emit('selectDashboard', { + path: '.gitlab/dashboards/file&path.yml', + display_name: 'dashboard©.yml', }); + + expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`); }); }); - describe('actions menu', () => { + describe('environments dropdown', () => { beforeEach(() => { - store.state.monitoringDashboard.projectPath = ''; createShallowWrapper(); }); - it('is rendered if projectPath is set in store', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + it('shows the environments dropdown', () => { + expect(findEnvsDropdown().exists()).toBe(true); + }); - return wrapper.vm.$nextTick().then(() => { - expect(findActionsMenu().exists()).toBe(true); + it('renders a search input', () => { + expect(findEnvsDropdownSearch().exists()).toBe(true); + }); + + describe('when environments data is not loaded', () => { + beforeEach(() => { + setupStoreWithDashboard(store); + return wrapper.vm.$nextTick(); + }); + + it('there are no environments listed', () => { + expect(findEnvsDropdownItems()).toHaveLength(0); + }); + }); + + describe('when environments data is loaded', () => { + const currentDashboard = dashboardGitResponse[0].path; + const currentEnvironmentName = environmentData[0].name; + + beforeEach(() => { + setupStoreWithData(store); + store.state.monitoringDashboard.projectPath = mockProjectPath; + store.state.monitoringDashboard.currentDashboard = currentDashboard; + store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName; + + return wrapper.vm.$nextTick(); + }); + + it('renders dropdown items with the environment name', () => { + const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`; + + findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => { + const { name, id } = environmentData[index]; + const idParam = encodeURIComponent(id); + + expect(itemWrapper.text()).toBe(name); + expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`); + }); + }); + + it('environments dropdown items can be checked', () => { + const items = findEnvsDropdownItems(); + const checkItems = findEnvsDropdownItems().filter(item => item.props('isCheckItem')); + + expect(items).toHaveLength(checkItems.length); + }); + + it('checks the currently selected environment', () => { + const selectedItems = findEnvsDropdownItems().filter(item => item.props('isChecked')); + + expect(selectedItems).toHaveLength(1); + expect(selectedItems.at(0).text()).toBe(currentEnvironmentName); + }); + + it('filters rendered dropdown items', () => { + const searchTerm = 'production'; + const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1); + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick().then(() => { + expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length); + }); + }); + + it('does not filter dropdown items if search term is empty string', () => { + const searchTerm = ''; + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick(() => { + expect(findEnvsDropdownItems()).toHaveLength(environmentData.length); + }); + }); + + it("shows error message if search term doesn't match", () => { + const searchTerm = 'does-not-exist'; + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick(() => { + expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true); + }); + }); + + it('shows loading element when environments fetch is still loading', () => { + store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`); + + return wrapper.vm + .$nextTick() + .then(() => { + expect(findEnvsDropdownLoadingIcon().exists()).toBe(true); + }) + .then(() => { + store.commit( + `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, + environmentData, + ); + }) + .then(() => { + expect(findEnvsDropdownLoadingIcon().exists()).toBe(false); + }); }); }); + }); - it('is not rendered if projectPath is not set in store', () => { - expect(findActionsMenu().exists()).toBe(false); + describe('date time picker', () => { + beforeEach(() => { + createShallowWrapper(); }); - it('contains a modal', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + it('is rendered', () => { + expect(findDateTimePicker().exists()).toBe(true); + }); - return wrapper.vm.$nextTick().then(() => { - expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true); + describe('timezone setting', () => { + const setupWithTimezone = value => { + store = createStore({ dashboardTimezone: value }); + createShallowWrapper(); + }; + + describe('local timezone is enabled by default', () => { + it('shows the data time picker in local timezone', () => { + expect(findDateTimePicker().props('utc')).toBe(false); + }); + }); + + describe('when LOCAL timezone is enabled', () => { + beforeEach(() => { + setupWithTimezone('LOCAL'); + }); + + it('shows the data time picker in local timezone', () => { + expect(findDateTimePicker().props('utc')).toBe(false); + }); + }); + + describe('when UTC timezone is enabled', () => { + beforeEach(() => { + setupWithTimezone('UTC'); + }); + + it('shows the data time picker in UTC format', () => { + expect(findDateTimePicker().props('utc')).toBe(true); + }); }); }); + }); + + describe('refresh button', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + it('is rendered', () => { + expect(findRefreshButton().exists()).toBe(true); + }); + }); + + describe('external dashboard link', () => { + beforeEach(() => { + store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl'; + createShallowWrapper(); + + return wrapper.vm.$nextTick(); + }); + + it('shows the link', () => { + const externalDashboardButton = wrapper.find('.js-external-dashboard-link'); + + expect(externalDashboardButton.exists()).toBe(true); + expect(externalDashboardButton.is(GlButton)).toBe(true); + expect(externalDashboardButton.text()).toContain('View full dashboard'); + }); + }); - const duplicableCases = [ - null, // When no path is specified, it uses the default dashboard path. + describe('actions menu', () => { + const ootbDashboards = [ dashboardGitResponse[0].path, - dashboardGitResponse[2].path, selfMonitoringDashboardGitResponse[0].path, ]; + const customDashboards = [ + dashboardGitResponse[1].path, + selfMonitoringDashboardGitResponse[1].path, + ]; - describe.each(duplicableCases)( - 'when the selected dashboard can be duplicated', - dashboardPath => { - it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; - setupAllDashboards(store, dashboardPath); + it('is rendered', () => { + createShallowWrapper(); - return wrapper.vm.$nextTick().then(() => { - expect(findCreateDashboardMenuItem().exists()).toBe(true); - expect(findCreateDashboardDuplicateItem().exists()).toBe(true); - }); + expect(findActionsMenu().exists()).toBe(true); + }); + + describe('adding metrics prop', () => { + it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', dashboardPath => { + createShallowWrapper({ customMetricsAvailable: true }); + + store.state.monitoringDashboard.emptyState = false; + setupAllDashboards(store, dashboardPath); + + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true); }); - }, - ); + }); - const nonDuplicableCases = [ - dashboardGitResponse[1].path, - selfMonitoringDashboardGitResponse[1].path, - ]; + it.each(customDashboards)( + 'gets passed false if current dashboard is custom', + dashboardPath => { + createShallowWrapper({ customMetricsAvailable: true }); - describe.each(nonDuplicableCases)( - 'when the selected dashboard cannot be duplicated', - dashboardPath => { - it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + store.state.monitoringDashboard.emptyState = false; setupAllDashboards(store, dashboardPath); return wrapper.vm.$nextTick().then(() => { - expect(findCreateDashboardMenuItem().exists()).toBe(true); - expect(findCreateDashboardDuplicateItem().exists()).toBe(false); + expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false); }); + }, + ); + + it('gets passed false if empty state is shown', () => { + createShallowWrapper({ customMetricsAvailable: true }); + + store.state.monitoringDashboard.emptyState = true; + setupAllDashboards(store, ootbDashboards[0]); + + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false); }); - }, - ); - }); + }); - describe('actions menu modals', () => { - const url = 'https://path/to/project'; + it('gets passed false if custom metrics are not available', () => { + createShallowWrapper({ customMetricsAvailable: false }); - beforeEach(() => { - store.state.monitoringDashboard.projectPath = url; - setupAllDashboards(store); + store.state.monitoringDashboard.emptyState = false; + setupAllDashboards(store, ootbDashboards[0]); - createShallowWrapper(); + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false); + }); + }); }); - it('Clicking on "Create New" opens up a modal', () => { - const modalId = 'createDashboard'; - const modalTrigger = findCreateDashboardMenuItem(); - const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + it('custom metrics path gets passed', () => { + const path = 'https://path/to/customMetrics'; - modalTrigger.trigger('click'); + createShallowWrapper({ customMetricsPath: path }); return wrapper.vm.$nextTick().then(() => { - expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + expect(findActionsMenu().props('customMetricsPath')).toBe(path); }); }); - it('"Create new dashboard" modal contains correct buttons', () => { - expect(findCreateDashboardModal().props('projectPath')).toBe(url); + it('validate query path gets passed', () => { + const path = 'https://path/to/validateQuery'; + + createShallowWrapper({ validateQueryPath: path }); + + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().props('validateQueryPath')).toBe(path); + }); }); - it('"Duplicate Dashboard" opens up a modal', () => { - const modalId = 'duplicateDashboard'; - const modalTrigger = findCreateDashboardDuplicateItem(); - const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + it('default branch gets passed', () => { + const branch = 'branchName'; - modalTrigger.trigger('click'); + createShallowWrapper({ defaultBranch: branch }); return wrapper.vm.$nextTick().then(() => { - expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + expect(findActionsMenu().props('defaultBranch')).toBe(branch); }); }); }); diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js new file mode 100644 index 00000000000..587ddd23d3f --- /dev/null +++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js @@ -0,0 +1,234 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui'; +import { createStore } from '~/monitoring/stores'; +import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; +import * as types from '~/monitoring/stores/mutation_types'; +import { metricsDashboardResponse } from '../fixture_data'; +import { mockTimeRange } from '../mock_data'; + +import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue'; +import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; + +const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0]; + +describe('dashboard invalid url parameters', () => { + let store; + let wrapper; + let mockShowToast; + + const createComponent = (props = {}, options = {}) => { + wrapper = shallowMount(DashboardPanelBuilder, { + propsData: { ...props }, + store, + stubs: { + GlCard, + }, + mocks: { + $toast: { + show: mockShowToast, + }, + }, + options, + }); + }; + + const findForm = () => wrapper.find(GlForm); + const findTxtArea = () => findForm().find(GlFormTextarea); + const findSubmitBtn = () => findForm().find('[type="submit"]'); + const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' }); + const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' }); + const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' }); + const findPanel = () => wrapper.find(DashboardPanel); + const findTimeRangePicker = () => wrapper.find(DateTimePicker); + const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]'); + + beforeEach(() => { + mockShowToast = jest.fn(); + store = createStore(); + createComponent(); + jest.spyOn(store, 'dispatch').mockResolvedValue(); + }); + + afterEach(() => {}); + + it('is mounted', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('displays an empty dashboard panel', () => { + expect(findPanel().exists()).toBe(true); + expect(findPanel().props('graphData')).toBe(null); + }); + + it('does not fetch initial data by default', () => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + describe('yml form', () => { + it('form exists and can be submitted', () => { + expect(findForm().exists()).toBe(true); + expect(findSubmitBtn().exists()).toBe(true); + expect(findSubmitBtn().is('[disabled]')).toBe(false); + }); + + it('form has a text area with a default value', () => { + expect(findTxtArea().exists()).toBe(true); + + const value = findTxtArea().attributes('value'); + + // Panel definition should contain a title and a type + expect(value).toContain('title:'); + expect(value).toContain('type:'); + }); + + it('"copy to clipboard" button works', () => { + findClipboardCopyBtn().vm.$emit('click'); + const clipboardText = findClipboardCopyBtn().attributes('data-clipboard-text'); + + expect(clipboardText).toContain('title:'); + expect(clipboardText).toContain('type:'); + + expect(mockShowToast).toHaveBeenCalledTimes(1); + }); + + it('on submit fetches a panel preview', () => { + findForm().vm.$emit('submit', new Event('submit')); + + return wrapper.vm.$nextTick().then(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/fetchPanelPreview', + expect.stringContaining('title:'), + ); + }); + }); + + describe('when form is submitted', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content'); + return wrapper.vm.$nextTick(); + }); + + it('submit button is disabled', () => { + expect(findSubmitBtn().is('[disabled]')).toBe(true); + }); + }); + }); + + describe('time range picker', () => { + it('is visible by default', () => { + expect(findTimeRangePicker().exists()).toBe(true); + }); + + it('when changed does not trigger data fetch unless preview panel button is clicked', () => { + // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false + store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false); + + return wrapper.vm.$nextTick(() => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + it('when changed triggers data fetch if preview panel button is clicked', () => { + findForm().vm.$emit('submit', new Event('submit')); + + store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange); + + return wrapper.vm.$nextTick(() => { + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + }); + + describe('refresh', () => { + it('is visible by default', () => { + expect(findRefreshButton().exists()).toBe(true); + }); + + it('when clicked does not trigger data fetch unless preview panel button is clicked', () => { + // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false + store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false); + + return wrapper.vm.$nextTick(() => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + + it('when clicked triggers data fetch if preview panel button is clicked', () => { + // mimic state where preview is visible. SET_PANEL_PREVIEW_IS_SHOWN is set to true + store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, true); + + findRefreshButton().vm.$emit('click'); + + return wrapper.vm.$nextTick(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/fetchPanelPreviewMetrics', + undefined, + ); + }); + }); + }); + + describe('instructions card', () => { + const mockDocsPath = '/docs-path'; + const mockProjectPath = '/project-path'; + + beforeEach(() => { + store.state.monitoringDashboard.addDashboardDocumentationPath = mockDocsPath; + store.state.monitoringDashboard.projectPath = mockProjectPath; + + createComponent(); + }); + + it('displays next actions for the user', () => { + expect(findViewDocumentationBtn().exists()).toBe(true); + expect(findViewDocumentationBtn().attributes('href')).toBe(mockDocsPath); + + expect(findOpenRepositoryBtn().exists()).toBe(true); + expect(findOpenRepositoryBtn().attributes('href')).toBe(mockProjectPath); + }); + }); + + describe('when there is an error', () => { + const mockError = 'an error ocurred!'; + + beforeEach(() => { + store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError); + return wrapper.vm.$nextTick(); + }); + + it('displays an alert', () => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + expect(wrapper.find(GlAlert).text()).toBe(mockError); + }); + + it('displays an empty dashboard panel', () => { + expect(findPanel().props('graphData')).toBe(null); + }); + + it('changing time range should not refetch data', () => { + store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange); + + return wrapper.vm.$nextTick(() => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when panel data is available', () => { + beforeEach(() => { + store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel); + return wrapper.vm.$nextTick(); + }); + + it('displays no alert', () => { + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); + + it('displays panel with data', () => { + const { title, type } = wrapper.find(DashboardPanel).props('graphData'); + + expect(title).toBe(mockPanel.title); + expect(type).toBe(mockPanel.type); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js index 693818aa55a..fb96bcc042f 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js @@ -2,23 +2,23 @@ import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import { setTestTimeout } from 'helpers/timeout'; +import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui'; 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 { + mockAlert, mockLogsHref, mockLogsPath, mockNamespace, mockNamespacedData, mockTimeRange, - graphDataPrometheusQueryRangeMultiTrack, barMockData, } from '../mock_data'; import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data'; -import { anomalyGraphData, singleStatGraphData } from '../graph_data'; +import { anomalyGraphData, singleStatGraphData, heatmapGraphData } from '../graph_data'; import { panelTypes } from '~/monitoring/constants'; @@ -56,9 +56,10 @@ describe('Dashboard Panel', () => { const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' }); const findMenuItems = () => wrapper.findAll(GlDropdownItem); const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text); + const findAlertsWidget = () => wrapper.find(AlertWidget); - const createWrapper = (props, options) => { - wrapper = shallowMount(DashboardPanel, { + const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => { + wrapper = mountFn(DashboardPanel, { propsData: { graphData, settingsPath: dashboardProps.settingsPath, @@ -79,6 +80,9 @@ describe('Dashboard Panel', () => { }); }; + const setMetricsSavedToDb = val => + monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val); + beforeEach(() => { setTestTimeout(1000); @@ -235,7 +239,7 @@ describe('Dashboard Panel', () => { ${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false} ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false} ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false} - ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} | ${false} + ${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false} ${barMockData} | ${MonitorBarChart} | ${false} `('when $data.type data is provided', ({ data, component, hasCtxMenu }) => { const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' }; @@ -255,6 +259,35 @@ describe('Dashboard Panel', () => { }); }); }); + + describe('computed', () => { + describe('fixedCurrentTimeRange', () => { + it('returns fixed time for valid time range', () => { + state.timeRange = mockTimeRange; + return wrapper.vm.$nextTick(() => { + expect(findTimeChart().props('timeRange')).toEqual( + expect.objectContaining({ + start: expect.any(String), + end: expect.any(String), + }), + ); + }); + }); + + it.each` + input | output + ${''} | ${{}} + ${undefined} | ${{}} + ${null} | ${{}} + ${'2020-12-03'} | ${{}} + `('returns $output for invalid input like $input', ({ input, output }) => { + state.timeRange = input; + return wrapper.vm.$nextTick(() => { + expect(findTimeChart().props('timeRange')).toEqual(output); + }); + }); + }); + }); }); describe('Edit custom metric dropdown item', () => { @@ -444,7 +477,7 @@ describe('Dashboard Panel', () => { describe('csvText', () => { it('converts metrics data from json to csv', () => { - const header = `timestamp,${graphData.y_label}`; + const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].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]}`; @@ -523,7 +556,7 @@ describe('Dashboard Panel', () => { }); it('displays a heatmap in local timezone', () => { - createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack }); + createWrapper({ graphData: heatmapGraphData() }); expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL'); }); @@ -538,7 +571,7 @@ describe('Dashboard Panel', () => { }); it('displays a heatmap with UTC', () => { - createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack }); + createWrapper({ graphData: heatmapGraphData() }); expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC'); }); }); @@ -573,10 +606,6 @@ describe('Dashboard Panel', () => { }); describe('panel alerts', () => { - const setMetricsSavedToDb = val => - monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val); - const findAlertsWidget = () => wrapper.find(AlertWidget); - beforeEach(() => { mockGetterReturnValue('metricsSavedToDb', []); @@ -702,4 +731,60 @@ describe('Dashboard Panel', () => { expect(findManageLinksItem().exists()).toBe(false); }); }); + + describe('Runbook url', () => { + const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]'); + const { metricId } = graphData.metrics[0]; + const { alert_path: alertPath } = mockAlert; + + const mockRunbookAlert = { + ...mockAlert, + metricId, + }; + + beforeEach(() => { + mockGetterReturnValue('metricsSavedToDb', []); + }); + + it('does not show a runbook link when alerts are not present', () => { + createWrapper(); + + expect(findRunbookLinks().length).toBe(0); + }); + + describe('when alerts are present', () => { + beforeEach(() => { + setMetricsSavedToDb([metricId]); + + createWrapper({ + alertsEndpoint: '/endpoint', + prometheusAlertsAvailable: true, + }); + }); + + it('does not show a runbook link when a runbook is not set', async () => { + findAlertsWidget().vm.$emit('setAlerts', alertPath, { + ...mockRunbookAlert, + runbookUrl: '', + }); + + await wrapper.vm.$nextTick(); + + expect(findRunbookLinks().length).toBe(0); + }); + + it('shows a runbook link when a runbook is set', async () => { + findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert); + + await wrapper.vm.$nextTick(); + + expect(findRunbookLinks().length).toBe(1); + expect( + findRunbookLinks() + .at(0) + .attributes('href'), + ).toBe(invalidUrl); + }); + }); + }); }); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 4b7f7a9ddb3..f37d95317ab 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -1,19 +1,14 @@ import { shallowMount, mount } from '@vue/test-utils'; -import Tracking from '~/tracking'; -import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys'; -import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; -import { objectToQuery } from '~/lib/utils/url_utility'; import VueDraggable from 'vuedraggable'; import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import { ESC_KEY } from '~/lib/utils/keys'; +import { objectToQuery } from '~/lib/utils/url_utility'; import axios from '~/lib/utils/axios_utils'; import { dashboardEmptyStates, metricStates } from '~/monitoring/constants'; import Dashboard from '~/monitoring/components/dashboard.vue'; import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; -import RefreshButton from '~/monitoring/components/refresh_button.vue'; -import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; -import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; -import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import EmptyState from '~/monitoring/components/empty_state.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; @@ -29,14 +24,13 @@ import { setupStoreWithDataForPanelCount, setupStoreWithLinks, } from '../store_utils'; -import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data'; +import { dashboardGitResponse, storeVariables } from '../mock_data'; import { metricsDashboardViewModel, metricsDashboardPanelCount, dashboardProps, } from '../fixture_data'; -import createFlash from '~/flash'; -import { TEST_HOST } from 'helpers/test_constants'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; jest.mock('~/flash'); @@ -45,14 +39,6 @@ describe('Dashboard', () => { let wrapper; let mock; - const findDashboardHeader = () => wrapper.find(DashboardHeader); - const findEnvironmentsDropdown = () => - findDashboardHeader().find({ ref: 'monitorEnvironmentsDropdown' }); - const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem); - const setSearchTerm = searchTerm => { - store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); - }; - const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(Dashboard, { propsData: { ...dashboardProps, ...props }, @@ -90,28 +76,6 @@ describe('Dashboard', () => { } }); - describe('no metrics are available yet', () => { - beforeEach(() => { - createShallowWrapper(); - }); - - it('shows the environment selector', () => { - expect(findEnvironmentsDropdown().exists()).toBe(true); - }); - }); - - describe('no data found', () => { - beforeEach(() => { - createShallowWrapper(); - - return wrapper.vm.$nextTick(); - }); - - it('shows the environment selector dropdown', () => { - expect(findEnvironmentsDropdown().exists()).toBe(true); - }); - }); - describe('request information to the server', () => { it('calls to set time range and fetch data', () => { createShallowWrapper({ hasMetrics: true }); @@ -149,17 +113,14 @@ describe('Dashboard', () => { }); it('fetches the metrics data with proper time window', () => { - jest.spyOn(store, 'dispatch'); - createMountedWrapper({ hasMetrics: true }); - store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); - return wrapper.vm.$nextTick().then(() => { - expect(store.dispatch).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined); + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/setTimeRange', + expect.objectContaining({ duration: { seconds: 28800 } }), + ); }); }); }); @@ -427,37 +388,6 @@ describe('Dashboard', () => { ); }); }); - - describe('when custom dashboard is selected', () => { - const windowLocation = window.location; - const findDashboardDropdown = () => wrapper.find(DashboardHeader).find(DashboardsDropdown); - - beforeEach(() => { - store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { - projectPath: TEST_HOST, - }); - - delete window.location; - window.location = { ...windowLocation, assign: jest.fn() }; - createMountedWrapper(); - - return wrapper.vm.$nextTick(); - }); - - afterEach(() => { - window.location = windowLocation; - }); - - it('encodes dashboard param', () => { - findDashboardDropdown().vm.$emit('selectDashboard', { - path: '.gitlab/dashboards/dashboard©.yml', - display_name: 'dashboard©.yml', - }); - expect(window.location.assign).toHaveBeenCalledWith( - `${TEST_HOST}/-/metrics/dashboard%26copy.yml`, - ); - }); - }); }); describe('when all panels in the first group are loading', () => { @@ -500,21 +430,6 @@ describe('Dashboard', () => { return wrapper.vm.$nextTick(); }); - it('renders the environments dropdown with a number of environments', () => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length); - - findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => { - const anchorEl = itemWrapper.find('a'); - if (anchorEl.exists()) { - const href = anchorEl.attributes('href'); - const currentDashboard = encodeURIComponent(dashboardGitResponse[0].path); - const environmentId = encodeURIComponent(environmentData[index].id); - const url = `${TEST_HOST}/-/metrics/${currentDashboard}?environment=${environmentId}`; - expect(href).toBe(url); - } - }); - }); - it('it does not show loading icons in any group', () => { setupStoreWithData(store); @@ -524,127 +439,6 @@ describe('Dashboard', () => { }); }); }); - - // Note: This test is not working, .active does not show the active environment - // eslint-disable-next-line jest/no-disabled-tests - it.skip('renders the environments dropdown with a single active element', () => { - const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper => - itemWrapper.find('.active').exists(), - ); - - expect(activeItem.length).toBe(1); - }); - }); - - describe('star dashboards', () => { - const findToggleStar = () => wrapper.find(DashboardHeader).find({ ref: 'toggleStarBtn' }); - const findToggleStarIcon = () => findToggleStar().find(GlIcon); - - beforeEach(() => { - createShallowWrapper(); - setupAllDashboards(store); - }); - - it('toggle star button is shown', () => { - expect(findToggleStar().exists()).toBe(true); - expect(findToggleStar().props('disabled')).toBe(false); - }); - - it('toggle star button is disabled when starring is taking place', () => { - store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`); - - return wrapper.vm.$nextTick(() => { - expect(findToggleStar().exists()).toBe(true); - expect(findToggleStar().props('disabled')).toBe(true); - }); - }); - - describe('when the dashboard list is loaded', () => { - // Tooltip element should wrap directly - const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title'); - - beforeEach(() => { - setupAllDashboards(store); - jest.spyOn(store, 'dispatch'); - }); - - it('dispatches a toggle star action', () => { - findToggleStar().vm.$emit('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(store.dispatch).toHaveBeenCalledWith( - 'monitoringDashboard/toggleStarredValue', - undefined, - ); - }); - }); - - describe('when dashboard is not starred', () => { - beforeEach(() => { - store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { - currentDashboard: dashboardGitResponse[0].path, - }); - return wrapper.vm.$nextTick(); - }); - - it('toggle star button shows "Star dashboard"', () => { - expect(getToggleTooltip()).toBe('Star dashboard'); - }); - - it('toggle star button shows an unstarred state', () => { - expect(findToggleStarIcon().attributes('name')).toBe('star-o'); - }); - }); - - describe('when dashboard is starred', () => { - beforeEach(() => { - store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { - currentDashboard: dashboardGitResponse[1].path, - }); - return wrapper.vm.$nextTick(); - }); - - it('toggle star button shows "Star dashboard"', () => { - expect(getToggleTooltip()).toBe('Unstar dashboard'); - }); - - it('toggle star button shows a starred state', () => { - expect(findToggleStarIcon().attributes('name')).toBe('star'); - }); - }); - }); - }); - - it('hides the environments dropdown list when there is no environments', () => { - createMountedWrapper({ hasMetrics: true }); - - setupStoreWithDashboard(store); - - return wrapper.vm.$nextTick().then(() => { - expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); - }); - }); - - it('renders the datetimepicker dropdown', () => { - createMountedWrapper({ hasMetrics: true }); - - setupStoreWithData(store); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DateTimePicker).exists()).toBe(true); - }); - }); - - it('renders the refresh dashboard button', () => { - createMountedWrapper({ hasMetrics: true }); - - setupStoreWithData(store); - - return wrapper.vm.$nextTick().then(() => { - const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton); - - expect(refreshBtn.exists()).toBe(true); - }); }); describe('variables section', () => { @@ -772,15 +566,6 @@ describe('Dashboard', () => { undefined, ); }); - - it('restores dashboard from full screen by typing the Escape key on IE11', () => { - mockKeyup(ESC_KEY_IE11); - - expect(store.dispatch).toHaveBeenCalledWith( - `monitoringDashboard/clearExpandedPanel`, - undefined, - ); - }); }); }); @@ -811,100 +596,6 @@ describe('Dashboard', () => { }); }); - describe('searchable environments dropdown', () => { - beforeEach(() => { - createMountedWrapper({ hasMetrics: true }, { attachToDocument: true }); - - setupStoreWithData(store); - - return wrapper.vm.$nextTick(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a search input', () => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownSearch' }) - .exists(), - ).toBe(true); - }); - - it('renders dropdown items', () => { - findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => { - const anchorEl = itemWrapper.find('a'); - if (anchorEl.exists()) { - expect(anchorEl.text()).toBe(environmentData[index].name); - } - }); - }); - - it('filters rendered dropdown items', () => { - const searchTerm = 'production'; - const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1); - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick().then(() => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length); - }); - }); - - it('does not filter dropdown items if search term is empty string', () => { - const searchTerm = ''; - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick(() => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length); - }); - }); - - it("shows error message if search term doesn't match", () => { - const searchTerm = 'does-not-exist'; - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownMsg' }) - .isVisible(), - ).toBe(true); - }); - }); - - it('shows loading element when environments fetch is still loading', () => { - store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`); - - return wrapper.vm - .$nextTick() - .then(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownLoading' }) - .exists(), - ).toBe(true); - }) - .then(() => { - store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); - }) - .then(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownLoading' }) - .exists(), - ).toBe(false); - }); - }); - }); - describe('drag and drop function', () => { const findDraggables = () => wrapper.findAll(VueDraggable); const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled')); @@ -998,57 +689,6 @@ describe('Dashboard', () => { }); }); - describe('dashboard timezone', () => { - const setupWithTimezone = value => { - store = createStore({ dashboardTimezone: value }); - setupStoreWithData(store); - createShallowWrapper({ hasMetrics: true }); - return wrapper.vm.$nextTick; - }; - - describe('local timezone is enabled by default', () => { - beforeEach(() => { - return setupWithTimezone(); - }); - - it('shows the data time picker in local timezone', () => { - expect( - findDashboardHeader() - .find(DateTimePicker) - .props('utc'), - ).toBe(false); - }); - }); - - describe('when LOCAL timezone is enabled', () => { - beforeEach(() => { - return setupWithTimezone('LOCAL'); - }); - - it('shows the data time picker in local timezone', () => { - expect( - findDashboardHeader() - .find(DateTimePicker) - .props('utc'), - ).toBe(false); - }); - }); - - describe('when UTC timezone is enabled', () => { - beforeEach(() => { - return setupWithTimezone('UTC'); - }); - - it('shows the data time picker in UTC format', () => { - expect( - findDashboardHeader() - .find(DateTimePicker) - .props('utc'), - ).toBe(true); - }); - }); - }); - describe('cluster health', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true, showHeader: false }); @@ -1068,36 +708,9 @@ describe('Dashboard', () => { }); }); - describe('dashboard edit link', () => { - const findEditLink = () => wrapper.find('.js-edit-link'); - - beforeEach(() => { - createShallowWrapper({ hasMetrics: true }); - - setupAllDashboards(store); - return wrapper.vm.$nextTick(); - }); - - it('is not present for the default dashboard', () => { - expect(findEditLink().exists()).toBe(false); - }); - - it('is present for a custom dashboard, and links to its edit_path', () => { - const dashboard = dashboardGitResponse[1]; - store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, { - currentDashboard: dashboard.path, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(findEditLink().exists()).toBe(true); - expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path); - }); - }); - }); - describe('document title', () => { const originalTitle = 'Original Title'; - const defaultDashboardName = dashboardGitResponse[0].display_name; + const overviewDashboardName = dashboardGitResponse[0].display_name; beforeEach(() => { document.title = originalTitle; @@ -1108,11 +721,11 @@ describe('Dashboard', () => { document.title = ''; }); - it('is prepended with default dashboard name by default', () => { + it('is prepended with the overview dashboard name by default', () => { setupAllDashboards(store); return wrapper.vm.$nextTick().then(() => { - expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true); + expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true); }); }); @@ -1127,11 +740,11 @@ describe('Dashboard', () => { }); }); - it('is prepended with default dashboard name is path is not known', () => { + it('is prepended with the overview dashboard name if path is not known', () => { setupAllDashboards(store, 'unknown/path'); return wrapper.vm.$nextTick().then(() => { - expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true); + expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true); }); }); @@ -1151,41 +764,6 @@ describe('Dashboard', () => { }); }); - describe('Dashboard dropdown', () => { - beforeEach(() => { - createMountedWrapper({ hasMetrics: true }); - setupAllDashboards(store); - return wrapper.vm.$nextTick(); - }); - - it('shows the dashboard dropdown', () => { - const dashboardDropdown = wrapper.find(DashboardsDropdown); - - expect(dashboardDropdown.exists()).toBe(true); - }); - }); - - describe('external dashboard link', () => { - beforeEach(() => { - createMountedWrapper({ - hasMetrics: true, - showPanels: false, - showTimeWindowDropdown: false, - externalDashboardUrl: '/mockUrl', - }); - - return wrapper.vm.$nextTick(); - }); - - it('shows the link', () => { - const externalDashboardButton = wrapper.find('.js-external-dashboard-link'); - - expect(externalDashboardButton.exists()).toBe(true); - expect(externalDashboardButton.is(GlDeprecatedButton)).toBe(true); - expect(externalDashboardButton.text()).toContain('View full dashboard'); - }); - }); - describe('Clipboard text in panels', () => { const currentDashboard = dashboardGitResponse[1].path; const panelIndex = 1; // skip expanded panel @@ -1243,74 +821,4 @@ describe('Dashboard', () => { expect(dashboardPanel.exists()).toBe(true); }); }); - - describe('add custom metrics', () => { - const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' }); - - describe('when not available', () => { - beforeEach(() => { - createShallowWrapper({ - hasMetrics: true, - customMetricsPath: '/endpoint', - }); - }); - it('does not render add button on the dashboard', () => { - expect(findAddMetricButton().exists()).toBe(false); - }); - }); - - describe('when available', () => { - let origPage; - beforeEach(done => { - jest.spyOn(Tracking, 'event').mockReturnValue(); - createShallowWrapper({ - hasMetrics: true, - customMetricsPath: '/endpoint', - customMetricsAvailable: true, - }); - setupStoreWithData(store); - - origPage = document.body.dataset.page; - document.body.dataset.page = 'projects:environments:metrics'; - - wrapper.vm.$nextTick(done); - }); - afterEach(() => { - document.body.dataset.page = origPage; - }); - - it('renders add button on the dashboard', () => { - expect(findAddMetricButton()).toBeDefined(); - }); - - it('uses modal for custom metrics form', () => { - expect(wrapper.find(GlModal).exists()).toBe(true); - expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric'); - }); - it('adding new metric is tracked', done => { - const submitButton = wrapper - .find(DashboardHeader) - .find({ ref: 'submitCustomMetricsFormBtn' }).vm; - wrapper.vm.$nextTick(() => { - submitButton.$el.click(); - wrapper.vm.$nextTick(() => { - expect(Tracking.event).toHaveBeenCalledWith( - document.body.dataset.page, - 'click_button', - { - label: 'add_new_metric', - property: 'modal', - value: undefined, - }, - ); - done(); - }); - }); - }); - - it('renders custom metrics form fields', () => { - expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true); - }); - }); - }); }); diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js index 276e20bae6a..c4630bde32f 100644 --- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js +++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { queryToObject, redirectTo, diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js index d09fcc92ee7..89adbad386f 100644 --- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js +++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js @@ -1,12 +1,11 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDropdownItem, GlIcon } from '@gitlab/ui'; +import { GlNewDropdownItem, GlIcon } from '@gitlab/ui'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; -import { dashboardGitResponse, selfMonitoringDashboardGitResponse } from '../mock_data'; +import { dashboardGitResponse } from '../mock_data'; const defaultBranch = 'master'; -const modalId = 'duplicateDashboardModalId'; const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred); const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred); @@ -17,20 +16,16 @@ describe('DashboardsDropdown', () => { function createComponent(props, opts = {}) { const storeOpts = { - methods: { - duplicateSystemDashboard: jest.fn(), - }, computed: { allDashboards: () => mockDashboards, selectedDashboard: () => mockSelectedDashboard, }, }; - return shallowMount(DashboardsDropdown, { + wrapper = shallowMount(DashboardsDropdown, { propsData: { ...props, defaultBranch, - modalId, }, sync: false, ...storeOpts, @@ -38,8 +33,8 @@ describe('DashboardsDropdown', () => { }); } - const findItems = () => wrapper.findAll(GlDropdownItem); - const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i); + const findItems = () => wrapper.findAll(GlNewDropdownItem); + const findItemAt = i => wrapper.findAll(GlNewDropdownItem).at(i); const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' }); const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' }); const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' }); @@ -52,7 +47,7 @@ describe('DashboardsDropdown', () => { describe('when it receives dashboards data', () => { beforeEach(() => { - wrapper = createComponent(); + createComponent(); }); it('displays an item for each dashboard', () => { @@ -78,7 +73,7 @@ describe('DashboardsDropdown', () => { }); it('filters dropdown items when searched for item exists in the list', () => { - const searchTerm = 'Default'; + const searchTerm = 'Overview'; setSearchTerm(searchTerm); return wrapper.vm.$nextTick().then(() => { @@ -96,10 +91,22 @@ describe('DashboardsDropdown', () => { }); }); + describe('when a dashboard is selected', () => { + beforeEach(() => { + [mockSelectedDashboard] = starredDashboards; + createComponent(); + }); + + it('dashboard item is selected', () => { + expect(findItemAt(0).props('isChecked')).toBe(true); + expect(findItemAt(1).props('isChecked')).toBe(false); + }); + }); + describe('when the dashboard is missing a display name', () => { beforeEach(() => { mockDashboards = dashboardGitResponse.map(d => ({ ...d, display_name: undefined })); - wrapper = createComponent(); + createComponent(); }); it('displays items with the dashboard path, with starred dashboards first', () => { @@ -112,7 +119,7 @@ describe('DashboardsDropdown', () => { describe('when it receives starred dashboards', () => { beforeEach(() => { mockDashboards = starredDashboards; - wrapper = createComponent(); + createComponent(); }); it('displays an item for each dashboard', () => { @@ -133,7 +140,7 @@ describe('DashboardsDropdown', () => { describe('when it receives only not-starred dashboards', () => { beforeEach(() => { mockDashboards = notStarredDashboards; - wrapper = createComponent(); + createComponent(); }); it('displays an item for each dashboard', () => { @@ -150,90 +157,9 @@ describe('DashboardsDropdown', () => { }); }); - const duplicableCases = [ - dashboardGitResponse[0], - dashboardGitResponse[2], - selfMonitoringDashboardGitResponse[0], - ]; - - describe.each(duplicableCases)('when the selected dashboard can be duplicated', dashboard => { - let duplicateDashboardAction; - let modalDirective; - - beforeEach(() => { - mockSelectedDashboard = dashboard; - modalDirective = jest.fn(); - duplicateDashboardAction = jest.fn().mockResolvedValue(); - - wrapper = createComponent( - {}, - { - directives: { - GlModal: modalDirective, - }, - methods: { - // Mock vuex actions - duplicateSystemDashboard: duplicateDashboardAction, - }, - }, - ); - }); - - it('displays a dropdown item for each dashboard', () => { - expect(findItems().length).toEqual(dashboardGitResponse.length + 1); - }); - - it('displays one "duplicate dashboard" dropdown item with a directive attached', () => { - const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]'); - - expect(item.length).toBe(1); - }); - - it('"duplicate dashboard" dropdown item directive works', () => { - const item = wrapper.find('[data-testid="duplicateDashboardItem"]'); - - item.trigger('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(modalDirective).toHaveBeenCalled(); - }); - }); - - it('id is correct, as the value of modal directive binding matches modal id', () => { - expect(modalDirective).toHaveBeenCalledTimes(1); - - // Binding's second argument contains the modal id - expect(modalDirective.mock.calls[0][1]).toEqual( - expect.objectContaining({ - value: modalId, - }), - ); - }); - }); - - const nonDuplicableCases = [dashboardGitResponse[1], selfMonitoringDashboardGitResponse[1]]; - - describe.each(nonDuplicableCases)( - 'when the selected dashboard can not be duplicated', - dashboard => { - beforeEach(() => { - mockSelectedDashboard = dashboard; - - wrapper = createComponent(); - }); - - it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => { - const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]'); - - expect(findItems()).toHaveLength(dashboardGitResponse.length); - expect(item.length).toBe(0); - }); - }, - ); - describe('when a dashboard gets selected by the user', () => { beforeEach(() => { - wrapper = createComponent(); + createComponent(); findItemAt(1).vm.$emit('click'); }); diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js index 4e7fee81d66..74f265930b1 100644 --- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js +++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js @@ -1,10 +1,10 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; -import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import { TEST_HOST } from 'helpers/test_constants'; +import { setHTMLFixture } from 'helpers/fixtures'; +import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; import { groups, initialState, metricsData, metricsWithData } from './mock_data'; -import { setHTMLFixture } from 'helpers/fixtures'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js index 81f5d90c310..86e2523f708 100644 --- a/spec/frontend/monitoring/components/graph_group_spec.js +++ b/spec/frontend/monitoring/components/graph_group_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import GraphGroup from '~/monitoring/components/graph_group.vue'; import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import GraphGroup from '~/monitoring/components/graph_group.vue'; describe('Graph group component', () => { let wrapper; diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js index e8ef8192067..90bd6f67196 100644 --- a/spec/frontend/monitoring/components/group_empty_state_spec.js +++ b/spec/frontend/monitoring/components/group_empty_state_spec.js @@ -24,7 +24,7 @@ describe('GroupEmptyState', () => { 'FOO STATE', // does not fail with unknown states ]; - test.each(supportedStates)('Renders an empty state for %s', selectedState => { + it.each(supportedStates)('Renders an empty state for %s', selectedState => { const wrapper = createComponent({ selectedState }); expect(wrapper.element).toMatchSnapshot(); diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js index 29615638453..a9b8295f38e 100644 --- a/spec/frontend/monitoring/components/refresh_button_spec.js +++ b/spec/frontend/monitoring/components/refresh_button_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; -import { createStore } from '~/monitoring/stores'; +import Visibility from 'visibilityjs'; import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui'; - +import { createStore } from '~/monitoring/stores'; import RefreshButton from '~/monitoring/components/refresh_button.vue'; describe('RefreshButton', () => { @@ -10,8 +10,8 @@ describe('RefreshButton', () => { let dispatch; let documentHidden; - const createWrapper = () => { - wrapper = shallowMount(RefreshButton, { store }); + const createWrapper = (options = {}) => { + wrapper = shallowMount(RefreshButton, { store, ...options }); }; const findRefreshBtn = () => wrapper.find(GlButton); @@ -31,14 +31,8 @@ describe('RefreshButton', () => { jest.spyOn(store, 'dispatch').mockResolvedValue(); dispatch = store.dispatch; - // Document can be mock hidden by overriding the `hidden` property documentHidden = false; - Object.defineProperty(document, 'hidden', { - configurable: true, - get() { - return documentHidden; - }, - }); + jest.spyOn(Visibility, 'hidden').mockImplementation(() => documentHidden); createWrapper(); }); @@ -57,6 +51,20 @@ describe('RefreshButton', () => { expect(findDropdown().props('text')).toBe('Off'); }); + describe('when feature flag disable_metric_dashboard_refresh_rate is on', () => { + beforeEach(() => { + createWrapper({ + provide: { + glFeatures: { disableMetricDashboardRefreshRate: true }, + }, + }); + }); + + it('refresh rate is not available', () => { + expect(findDropdown().exists()).toBe(false); + }); + }); + describe('refresh rate options', () => { it('presents multiple options', () => { expect(findOptions().length).toBeGreaterThan(1); diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js index cc384aef231..788f3abf617 100644 --- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js +++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; import DropdownField from '~/monitoring/components/variables/dropdown_field.vue'; describe('Custom variable component', () => { @@ -23,8 +23,8 @@ describe('Custom variable component', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdown = () => wrapper.find(GlDeprecatedDropdown); + const findDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem); it('renders dropdown element when all necessary props are passed', () => { createShallowWrapper(); |