diff options
Diffstat (limited to 'spec/frontend/alert_management')
3 files changed, 596 insertions, 0 deletions
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js new file mode 100644 index 00000000000..1e4c2e24ccb --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -0,0 +1,242 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import { GlAlert, GlLoadingIcon, GlDropdownItem, GlTable } from '@gitlab/ui'; +import AlertDetails from '~/alert_management/components/alert_details.vue'; +import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql'; +import createFlash from '~/flash'; + +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; +jest.mock('~/flash'); + +describe('AlertDetails', () => { + let wrapper; + const newIssuePath = 'root/alerts/-/issues/new'; + const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); + const findDetailsTable = () => wrapper.find(GlTable); + + function mountComponent({ + data, + createIssueFromAlertEnabled = false, + loading = false, + mountMethod = shallowMount, + stubs = {}, + } = {}) { + wrapper = mountMethod(AlertDetails, { + propsData: { + alertId: 'alertId', + projectPath: 'projectPath', + newIssuePath, + }, + data() { + return { alert: { ...mockAlert }, ...data }; + }, + provide: { + glFeatures: { createIssueFromAlertEnabled }, + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + const findCreatedIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]'); + + describe('Alert details', () => { + describe('when alert is null', () => { + beforeEach(() => { + mountComponent({ data: { alert: null } }); + }); + + it('shows an empty state', () => { + expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false); + }); + }); + + describe('when alert is present', () => { + beforeEach(() => { + mountComponent({ data: { alert: mockAlert } }); + }); + + it('renders a tab with overview information', () => { + expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true); + }); + + it('renders a tab with full alert information', () => { + expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true); + }); + + it('renders a title', () => { + expect(wrapper.find('[data-testid="title"]').text()).toBe(mockAlert.title); + }); + + it('renders a start time', () => { + expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="startTimeItem"]').props().time).toBe( + mockAlert.startedAt, + ); + }); + }); + + describe('individual alert fields', () => { + describe.each` + field | data | isShown + ${'eventCount'} | ${1} | ${true} + ${'eventCount'} | ${undefined} | ${false} + ${'monitoringTool'} | ${'New Relic'} | ${true} + ${'monitoringTool'} | ${undefined} | ${false} + ${'service'} | ${'Prometheus'} | ${true} + ${'service'} | ${undefined} | ${false} + `(`$desc`, ({ field, data, isShown }) => { + beforeEach(() => { + mountComponent({ data: { alert: { ...mockAlert, [field]: data } } }); + }); + + it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => { + if (isShown) { + expect(wrapper.find(`[data-testid="${field}"]`).text()).toBe(data.toString()); + } else { + expect(wrapper.find(`[data-testid="${field}"]`).exists()).toBe(false); + } + }); + }); + }); + + describe('Create issue from alert', () => { + describe('createIssueFromAlertEnabled feature flag enabled', () => { + it('should display a button that links to new issue page', () => { + mountComponent({ createIssueFromAlertEnabled: true }); + expect(findCreatedIssueBtn().exists()).toBe(true); + expect(findCreatedIssueBtn().attributes('href')).toBe(newIssuePath); + }); + }); + + describe('createIssueFromAlertEnabled feature flag disabled', () => { + it('should display a button that links to a new issue page', () => { + mountComponent({ createIssueFromAlertEnabled: false }); + expect(findCreatedIssueBtn().exists()).toBe(false); + }); + }); + }); + + describe('View full alert details', () => { + beforeEach(() => { + mountComponent({ data: { alert: mockAlert } }); + }); + it('should display a table of raw alert details data', () => { + wrapper.find('[data-testid="fullDetailsTab"]').trigger('click'); + expect(findDetailsTable().exists()).toBe(true); + }); + }); + + describe('loading state', () => { + beforeEach(() => { + mountComponent({ loading: true }); + }); + + it('displays a loading state when loading', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('error state', () => { + it('displays a error state correctly', () => { + mountComponent({ data: { errored: true } }); + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + + it('does not display an error when dismissed', () => { + mountComponent({ data: { errored: true, isErrorDismissed: true } }); + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); + }); + + describe('header', () => { + const findHeader = () => wrapper.find('[data-testid="alert-header"]'); + const stubs = { TimeAgoTooltip: '<span>now</span>' }; + + describe('individual header fields', () => { + describe.each` + severity | createdAt | monitoringTool | result + ${'MEDIUM'} | ${'2020-04-17T23:18:14.996Z'} | ${null} | ${'Medium • Reported now'} + ${'INFO'} | ${'2020-04-17T23:18:14.996Z'} | ${'Datadog'} | ${'Info • Reported now by Datadog'} + `( + `When severity=$severity, createdAt=$createdAt, monitoringTool=$monitoringTool`, + ({ severity, createdAt, monitoringTool, result }) => { + beforeEach(() => { + mountComponent({ + data: { alert: { ...mockAlert, severity, createdAt, monitoringTool } }, + mountMethod: mount, + stubs, + }); + }); + + it('header text is shown correctly', () => { + expect(findHeader().text()).toBe(result); + }); + }, + ); + }); + }); + }); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alert: mockAlert }, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findStatusDropdownItem().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatus, + variables: { + iid: 'alertId', + status: 'TRIGGERED', + projectPath: 'projectPath', + }, + }); + }); + + it('calls `createFlash` when request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findStatusDropdownItem().vm.$emit('click'); + + setImmediate(() => { + expect(createFlash).toHaveBeenCalledWith( + 'There was an error while updating the status of the alert. Please try again.', + ); + }); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js new file mode 100644 index 00000000000..c4630ac57fe --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -0,0 +1,325 @@ +import { mount } from '@vue/test-utils'; +import { + GlEmptyState, + GlTable, + GlAlert, + GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlIcon, + GlTab, +} from '@gitlab/ui'; +import { visitUrl } from '~/lib/utils/url_utility'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import createFlash from '~/flash'; +import AlertManagementList from '~/alert_management/components/alert_management_list.vue'; +import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants'; +import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql'; +import mockAlerts from '../mocks/alerts.json'; + +jest.mock('~/flash'); + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn().mockName('visitUrlMock'), + joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, +})); + +describe('AlertManagementList', () => { + let wrapper; + + const findAlertsTable = () => wrapper.find(GlTable); + const findAlerts = () => wrapper.findAll('table tbody tr'); + const findAlert = () => wrapper.find(GlAlert); + const findLoader = () => wrapper.find(GlLoadingIcon); + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findStatusFilterTabs = () => wrapper.findAll(GlTab); + const findDateFields = () => wrapper.findAll(TimeAgo); + const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); + const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + data = {}, + loading = false, + alertListStatusFilteringEnabled = false, + stubs = {}, + } = {}) { + wrapper = mount(AlertManagementList, { + propsData: { + projectPath: 'gitlab-org/gitlab', + enableAlertManagementPath: '/link', + emptyAlertSvgPath: 'illustration/path', + ...props, + }, + provide: { + glFeatures: { + alertListStatusFilteringEnabled, + }, + }, + data() { + return data; + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alerts: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('alert management feature renders empty state', () => { + it('shows empty state', () => { + expect(wrapper.find(GlEmptyState).exists()).toBe(true); + }); + }); + + describe('Status Filter Tabs', () => { + describe('alertListStatusFilteringEnabled feature flag enabled', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts }, + loading: false, + alertListStatusFilteringEnabled: true, + stubs: { + GlTab: true, + }, + }); + }); + + it('should display filter tabs for all statuses', () => { + const tabs = findStatusFilterTabs().wrappers; + tabs.forEach((tab, i) => { + expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title); + }); + }); + }); + + describe('alertListStatusFilteringEnabled feature flag disabled', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts }, + loading: false, + alertListStatusFilteringEnabled: false, + stubs: { + GlTab: true, + }, + }); + }); + + it('should NOT display tabs', () => { + expect(findStatusFilterTabs()).not.toExist(); + }); + }); + }); + + describe('Alerts table', () => { + it('loading state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: null }, + loading: true, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findLoader().exists()).toBe(true); + }); + + it('error state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: null, errored: true }, + loading: false, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlertsTable().text()).toContain('No alerts to display'); + expect(findLoader().exists()).toBe(false); + expect(findAlert().props().variant).toBe('danger'); + }); + + it('empty state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: [], errored: false }, + loading: false, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlertsTable().text()).toContain('No alerts to display'); + expect(findLoader().exists()).toBe(false); + expect(findAlert().props().variant).toBe('info'); + }); + + it('has data state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + expect(findLoader().exists()).toBe(false); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlerts()).toHaveLength(mockAlerts.length); + }); + + it('displays status dropdown', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + expect(findStatusDropdown().exists()).toBe(true); + }); + + it('shows correct severity icons', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(GlTable).exists()).toBe(true); + expect( + findAlertsTable() + .find(GlIcon) + .classes('icon-critical'), + ).toBe(true); + }); + }); + + it('renders severity text', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + + expect( + findSeverityFields() + .at(0) + .text(), + ).toBe('Critical'); + }); + + it('navigates to the detail page when alert row is clicked', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + + findAlerts() + .at(0) + .trigger('click'); + expect(visitUrl).toHaveBeenCalledWith('/1527542/details'); + }); + + describe('handle date fields', () => { + it('should display time ago dates when values provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: [ + { + iid: 1, + status: 'acknowledged', + startedAt: '2020-03-17T23:18:14.996Z', + endedAt: '2020-04-17T23:18:14.996Z', + severity: 'high', + }, + ], + errored: false, + }, + loading: false, + }); + expect(findDateFields().length).toBe(2); + }); + + it('should not display time ago dates when values not provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: [ + { + iid: 1, + status: 'acknowledged', + startedAt: null, + endedAt: null, + severity: 'high', + }, + ], + errored: false, + }, + loading: false, + }); + expect(findDateFields().exists()).toBe(false); + }); + }); + }); + + describe('updating the alert status', () => { + const iid = '1527542'; + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + iid, + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findFirstStatusOption().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatus, + variables: { + iid, + status: 'TRIGGERED', + projectPath: 'gitlab-org/gitlab', + }, + }); + }); + + it('calls `createFlash` when request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findFirstStatusOption().vm.$emit('click'); + + setImmediate(() => { + expect(createFlash).toHaveBeenCalledWith( + 'There was an error while updating the status of the alert. Please try again.', + ); + }); + }); + }); +}); diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json new file mode 100644 index 00000000000..b67e2cfc52e --- /dev/null +++ b/spec/frontend/alert_management/mocks/alerts.json @@ -0,0 +1,29 @@ +[ + { + "iid": "1527542", + "title": "SyntaxError: Invalid or unexpected token", + "severity": "CRITICAL", + "eventCount": 7, + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "TRIGGERED" + }, + { + "iid": "1527543", + "title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert", + "severity": "MEDIUM", + "eventCount": 1, + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "ACKNOWLEDGED" + }, + { + "iid": "1527544", + "title": "SyntaxError: Invalid or unexpected token", + "severity": "LOW", + "eventCount": 4, + "startedAt": "2020-04-17T23:18:14.996Z", + "endedAt": "2020-04-17T23:18:14.996Z", + "status": "RESOLVED" + } + ] |