diff --git a/ee/app/assets/javascripts/monitoring/components/alert_widget.vue b/ee/app/assets/javascripts/monitoring/components/alert_widget.vue index 577cb72acbddf0f6e3b2fe81a414867c87883ce3..bd46e52f8344efef182f3dcbcedc2d109a4a0a42 100644 --- a/ee/app/assets/javascripts/monitoring/components/alert_widget.vue +++ b/ee/app/assets/javascripts/monitoring/components/alert_widget.vue @@ -54,18 +54,11 @@ export default { .map(this.formatAlertSummary) .join(', '); }, - supportsComputedAlerts() { - return gon.features && gon.features.prometheusComputedAlerts; - }, }, created() { this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint }); this.fetchAlertData(); }, - beforeDestroy() { - // clean up external event listeners - document.removeEventListener('click', this.handleOutsideClick); - }, methods: { fetchAlertData() { this.isLoading = true; diff --git a/ee/spec/frontend/monitoring/alert_widget_spec.js b/ee/spec/frontend/monitoring/alert_widget_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..334f8db3ef2f8d02246e8af36de81324d543fa3e --- /dev/null +++ b/ee/spec/frontend/monitoring/alert_widget_spec.js @@ -0,0 +1,245 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import createFlash from '~/flash'; +import AlertWidget from 'ee/monitoring/components/alert_widget.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +const mockReadAlert = jest.fn(); +const mockCreateAlert = jest.fn(); +const mockUpdateAlert = jest.fn(); +const mockDeleteAlert = jest.fn(); + +jest.mock('~/flash'); +jest.mock( + 'ee/monitoring/services/alerts_service', + () => + function AlertsServiceMock() { + return { + readAlert: mockReadAlert, + createAlert: mockCreateAlert, + updateAlert: mockUpdateAlert, + deleteAlert: mockDeleteAlert, + }; + }, +); + +describe('AlertWidget', () => { + let wrapper; + + const metricId = '5'; + const alertPath = 'my/alert.json'; + const relevantQueries = [{ metricId, label: 'alert-label', alert_path: alertPath }]; + + const defaultProps = { + alertsEndpoint: '', + relevantQueries, + alertsToManage: {}, + modalId: 'alert-modal-1', + }; + + const propsWithAlert = { + relevantQueries, + }; + + const propsWithAlertData = { + relevantQueries, + alertsToManage: { + [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId }, + }, + }; + + const createComponent = propsData => { + wrapper = shallowMount(AlertWidget, { + propsData: { + ...defaultProps, + ...propsData, + }, + sync: false, + }); + }; + const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' }); + const findAlertErrorMessage = () => wrapper.find('.alert-error-message'); + const findCurrentSettings = () => wrapper.find('.alert-current-setting'); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.destroy(); + wrapper = null; + }); + + it('displays a loading spinner and disables form when fetching alerts', () => { + let resolveReadAlert; + mockReadAlert.mockReturnValue( + new Promise(resolve => { + resolveReadAlert = resolve; + }), + ); + createComponent(defaultProps); + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + expect(findWidgetForm().props('disabled')).toBe(true); + + resolveReadAlert({ operator: '=', threshold: 42 }); + }) + .then(() => waitForPromises()) + .then(() => { + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false); + expect(findWidgetForm().props('disabled')).toBe(false); + }); + }); + + it('displays an error message when fetch fails', () => { + mockReadAlert.mockRejectedValue(); + createComponent(propsWithAlert); + + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + + return waitForPromises().then(() => { + expect(createFlash).toHaveBeenCalled(); + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false); + }); + }); + + it('displays an alert summary when there is a single alert', () => { + mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 }); + createComponent(propsWithAlertData); + + expect(wrapper.text()).toContain('alert-label > 42'); + }); + + it('displays a combined alert summary when there are multiple alerts', () => { + mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 }); + const propsWithManyAlerts = { + relevantQueries: relevantQueries.concat([ + { metricId: '6', alert_path: 'my/alert2.json', label: 'alert-label2' }, + ]), + alertsToManage: { + 'my/alert.json': { + operator: '>', + threshold: 42, + alert_path: alertPath, + metricId, + }, + 'my/alert2.json': { + operator: '=', + threshold: 900, + alert_path: 'my/alert2.json', + metricId: '6', + }, + }, + }; + createComponent(propsWithManyAlerts); + + expect(findCurrentSettings().text()).toEqual('alert-label > 42, alert-label2 = 900'); + }); + + it('creates an alert with an appropriate handler', () => { + const alertParams = { + operator: '<', + threshold: 4, + prometheus_metric_id: '5', + }; + mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 }); + const fakeAlertPath = 'foo/bar'; + mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams }); + createComponent({ + alertsToManage: { + [fakeAlertPath]: { + alert_path: fakeAlertPath, + operator: '<', + threshold: 4, + prometheus_metric_id: '5', + metricId: '5', + }, + }, + }); + + findWidgetForm().vm.$emit('create', alertParams); + + expect(mockCreateAlert).toHaveBeenCalledWith(alertParams); + }); + + it('updates an alert with an appropriate handler', () => { + const alertParams = { operator: '<', threshold: 4, alert_path: alertPath }; + const newAlertParams = { operator: '=', threshold: 12 }; + mockReadAlert.mockResolvedValue(alertParams); + mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams }); + createComponent({ + ...propsWithAlertData, + alertsToManage: { + [alertPath]: { + alert_path: alertPath, + operator: '=', + threshold: 12, + metricId: '5', + }, + }, + }); + + findWidgetForm().vm.$emit('update', { + alert: alertPath, + ...newAlertParams, + prometheus_metric_id: '5', + }); + + expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams); + }); + + it('deletes an alert with an appropriate handler', () => { + const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 }; + mockReadAlert.mockResolvedValue(alertParams); + mockDeleteAlert.mockResolvedValue({}); + createComponent({ + ...propsWithAlert, + alertsToManage: { + [alertPath]: { + alert_path: alertPath, + operator: '>', + threshold: 42, + metricId: '5', + }, + }, + }); + + findWidgetForm().vm.$emit('delete', { alert: alertPath }); + + expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath); + expect(findAlertErrorMessage().exists()).toBe(false); + }); + + describe('when delete fails', () => { + beforeEach(() => { + const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 }; + mockReadAlert.mockResolvedValue(alertParams); + mockDeleteAlert.mockRejectedValue(); + + createComponent({ + ...propsWithAlert, + alertsToManage: { + [alertPath]: { + alert_path: alertPath, + operator: '>', + threshold: 42, + metricId: '5', + }, + }, + }); + + findWidgetForm().vm.$emit('delete', { alert: alertPath }); + }); + + it('shows error message', () => { + expect(findAlertErrorMessage().text()).toEqual('Error deleting alert'); + }); + + it('dismisses error message on cancel', () => { + findWidgetForm().vm.$emit('cancel'); + + return wrapper.vm.$nextTick().then(() => { + expect(findAlertErrorMessage().exists()).toBe(false); + }); + }); + }); +}); diff --git a/ee/spec/javascripts/monitoring/alert_widget_spec.js b/ee/spec/javascripts/monitoring/alert_widget_spec.js deleted file mode 100644 index e46c8a0dfbc4bd215652c966253eec4147db141b..0000000000000000000000000000000000000000 --- a/ee/spec/javascripts/monitoring/alert_widget_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import Vue from 'vue'; -import AlertWidget from 'ee/monitoring/components/alert_widget.vue'; -import AlertsService from 'ee/monitoring/services/alerts_service'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import waitForPromises from 'spec/helpers/wait_for_promises'; - -describe('AlertWidget', () => { - let AlertWidgetComponent; - let vm; - - const metricId = '5'; - const alertPath = 'my/alert.json'; - const relevantQueries = [{ metricId, label: 'alert-label', alert_path: alertPath }]; - - const props = { - alertsEndpoint: '', - relevantQueries, - alertsToManage: {}, - modalId: 'alert-modal-1', - }; - - const propsWithAlert = { - ...props, - relevantQueries, - }; - - const propsWithAlertData = { - ...props, - relevantQueries, - alertsToManage: { - [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId }, - }, - }; - - const mockSetAlerts = (path, data) => { - const alerts = data ? { [path]: data } : {}; - Vue.set(vm, 'alertsToManage', alerts); - }; - - beforeAll(() => { - AlertWidgetComponent = Vue.extend(AlertWidget); - }); - - beforeEach(() => { - setFixtures('<div id="alert-widget"></div>'); - }); - - afterEach(() => { - if (vm) vm.$destroy(); - }); - - it('displays a loading spinner when fetching alerts', done => { - let resolveReadAlert; - - spyOn(AlertsService.prototype, 'readAlert').and.returnValue( - new Promise(resolve => { - resolveReadAlert = resolve; - }), - ); - vm = mountComponent(AlertWidgetComponent, propsWithAlert, '#alert-widget'); - - // expect loading spinner to exist during fetch - expect(vm.isLoading).toBeTruthy(); - expect(vm.$refs.widgetForm.$props.disabled).toBe(true); - - expect(vm.$el.querySelector('.loading-container')).toBeVisible(); - - resolveReadAlert({ operator: '=', threshold: 42 }); - - // expect loading spinner to go away after fetch - setTimeout(() => - vm.$nextTick(() => { - expect(vm.isLoading).toEqual(false); - expect(vm.$el.querySelector('.loading-container')).toBeHidden(); - expect(vm.$refs.widgetForm.$props.disabled).toBe(false); - done(); - }), - ); - }); - - it('displays an error message when fetch fails', done => { - const spy = spyOnDependency(AlertWidget, 'createFlash'); - spyOn(AlertsService.prototype, 'readAlert').and.returnValue(Promise.reject()); - vm = mountComponent(AlertWidgetComponent, propsWithAlert, '#alert-widget'); - - setTimeout(() => - vm.$nextTick(() => { - expect(vm.isLoading).toEqual(false); - expect(spy).toHaveBeenCalled(); - done(); - }), - ); - }); - - it('displays an alert summary when there is a single alert', () => { - spyOn(AlertsService.prototype, 'readAlert').and.returnValue( - Promise.resolve({ operator: '>', threshold: 42 }), - ); - vm = mountComponent(AlertWidgetComponent, propsWithAlertData, '#alert-widget'); - - expect(vm.alertSummary).toBe('alert-label > 42'); - expect(vm.$el.querySelector('.alert-current-setting')).toBeVisible(); - }); - - it('displays a combined alert summary when there are multiple alerts', () => { - spyOn(AlertsService.prototype, 'readAlert').and.returnValue( - Promise.resolve({ operator: '>', threshold: 42 }), - ); - const propsWithManyAlerts = { - ...props, - relevantQueries: relevantQueries.concat([ - { metricId: '6', alert_path: 'my/alert2.json', label: 'alert-label2' }, - ]), - alertsToManage: { - 'my/alert.json': { - operator: '>', - threshold: 42, - alert_path: alertPath, - metricId, - }, - 'my/alert2.json': { - operator: '=', - threshold: 900, - alert_path: 'my/alert2.json', - metricId: '6', - }, - }, - }; - vm = mountComponent(AlertWidgetComponent, propsWithManyAlerts, '#alert-widget'); - - expect(vm.alertSummary).toBe('alert-label > 42, alert-label2 = 900'); - expect(vm.$el.querySelector('.alert-current-setting')).toBeVisible(); - }); - - it('creates an alert with an appropriate handler', done => { - const alertParams = { - operator: '<', - threshold: 4, - prometheus_metric_id: '5', - }; - - spyOn(AlertsService.prototype, 'createAlert').and.returnValue( - Promise.resolve({ alert_path: 'foo/bar', ...alertParams }), - ); - - vm = mountComponent(AlertWidgetComponent, props); - vm.$on('setAlerts', mockSetAlerts); - - vm.$refs.widgetForm.$emit('create', alertParams); - - expect(AlertsService.prototype.createAlert).toHaveBeenCalledWith(alertParams); - - waitForPromises() - .then(() => { - expect(vm.isLoading).toEqual(false); - done(); - }) - .catch(done.fail); - }); - - it('dismisses error message when action is cancelled', () => { - vm = mountComponent(AlertWidgetComponent, props); - vm.$on('setAlerts', mockSetAlerts); - vm.errorMessage = 'Mock error message.'; - - // widget modal is dismissed - vm.$refs.widgetForm.$emit('cancel'); - - expect(vm.errorMessage).toBe(null); - }); - - it('updates an alert with an appropriate handler', done => { - const alertParams = { operator: '<', threshold: 4, alert_path: alertPath }; - const newAlertParams = { operator: '=', threshold: 12 }; - - spyOn(AlertsService.prototype, 'readAlert').and.returnValue(Promise.resolve(alertParams)); - spyOn(AlertsService.prototype, 'updateAlert').and.returnValue( - Promise.resolve({ ...alertParams, ...newAlertParams }), - ); - - vm = mountComponent(AlertWidgetComponent, propsWithAlertData); - vm.$on('setAlerts', mockSetAlerts); - - vm.$refs.widgetForm.$emit('update', { - alert: alertPath, - ...newAlertParams, - prometheus_metric_id: '5', - }); - - expect(AlertsService.prototype.updateAlert).toHaveBeenCalledWith(alertPath, newAlertParams); - waitForPromises() - .then(() => { - expect(vm.isLoading).toEqual(false); - done(); - }) - .catch(done.fail); - }); - - it('deletes an alert with an appropriate handler', done => { - const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 }; - - spyOn(AlertsService.prototype, 'readAlert').and.returnValue(Promise.resolve(alertParams)); - spyOn(AlertsService.prototype, 'deleteAlert').and.returnValue(Promise.resolve({})); - - vm = mountComponent(AlertWidgetComponent, propsWithAlert); - vm.$on('setAlerts', mockSetAlerts); - - vm.$refs.widgetForm.$emit('delete', { alert: alertPath }); - - expect(AlertsService.prototype.deleteAlert).toHaveBeenCalledWith(alertPath); - waitForPromises() - .then(() => { - expect(vm.isLoading).toEqual(false); - expect(vm.alertSummary).toBeFalsy(); - done(); - }) - .catch(done.fail); - }); -});