diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx
new file mode 100644
index 0000000000..58fb6c1a28
--- /dev/null
+++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import StatusLabel from './StatusLabel';
+
+describe('StatusLabel', () => {
+ test('should render success', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('green');
+ expect(wrapper.text()).toEqual('Success');
+ });
+
+ test('should render failed', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('red');
+ expect(wrapper.text()).toEqual('Failed');
+ });
+
+ test('should render error', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('red');
+ expect(wrapper.text()).toEqual('Error');
+ });
+
+ test('should render running', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('SyncAltIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('blue');
+ expect(wrapper.text()).toEqual('Running');
+ });
+
+ test('should render pending', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('ClockIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('blue');
+ expect(wrapper.text()).toEqual('Pending');
+ });
+
+ test('should render waiting', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('ClockIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('grey');
+ expect(wrapper.text()).toEqual('Waiting');
+ });
+
+ test('should render canceled', () => {
+ const wrapper = mount();
+ expect(wrapper).toHaveLength(1);
+ expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1);
+ expect(wrapper.find('Label').prop('color')).toEqual('orange');
+ expect(wrapper.text()).toEqual('Canceled');
+ });
+});
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx
index 18f45a4c21..d7f37f9fab 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx
@@ -1,33 +1,17 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
-import {
- Button,
- Chip,
- TextList,
- TextListItem,
- TextListItemVariants,
- TextListVariants,
- Label,
-} from '@patternfly/react-core';
+import { Button } from '@patternfly/react-core';
import { t } from '@lingui/macro';
-
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
-import ChipGroup from '../../../components/ChipGroup';
-import ContentError from '../../../components/ContentError';
-import ContentLoading from '../../../components/ContentLoading';
-import CredentialChip from '../../../components/CredentialChip';
import {
Detail,
DetailList,
DeletedDetail,
- UserDateDetail,
} from '../../../components/DetailList';
import DeleteButton from '../../../components/DeleteButton';
import ErrorDetail from '../../../components/ErrorDetail';
-import LaunchButton from '../../../components/LaunchButton';
-import { VariablesDetail } from '../../../components/CodeMirrorInput';
import { NotificationTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { NOTIFICATION_TYPES } from '../constants';
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx
new file mode 100644
index 0000000000..d39bffe087
--- /dev/null
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx
@@ -0,0 +1,202 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { OrganizationsAPI } from '../../../api';
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import NotificationTemplateList from './NotificationTemplateList';
+
+jest.mock('../../../api');
+
+const mockTemplates = {
+ data: {
+ count: 3,
+ results: [
+ {
+ name: 'Boston',
+ id: 1,
+ url: '/notification_templates/1',
+ type: 'slack',
+ summary_fields: {
+ recent_notifications: [
+ {
+ status: 'success',
+ },
+ ],
+ user_capabilities: {
+ delete: true,
+ edit: true,
+ },
+ },
+ },
+ {
+ name: 'Minneapolis',
+ id: 2,
+ url: '/notification_templates/2',
+ summary_fields: {
+ recent_notifications: [],
+ user_capabilities: {
+ delete: true,
+ edit: true,
+ },
+ },
+ },
+ {
+ name: 'Philidelphia',
+ id: 3,
+ url: '/notification_templates/3',
+ summary_fields: {
+ recent_notifications: [
+ {
+ status: 'failed',
+ },
+ {
+ status: 'success',
+ },
+ ],
+ user_capabilities: {
+ delete: true,
+ edit: true,
+ },
+ },
+ },
+ ],
+ },
+};
+
+describe('', () => {
+ let wrapper;
+ beforeEach(() => {
+ OrganizationsAPI.read.mockResolvedValue(mockTemplates);
+ OrganizationsAPI.readOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ },
+ });
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should load notifications', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
+ expect(wrapper.find('NotificationTemplateListItem').length).toBe(3);
+ });
+
+ test('should select item', async () => {
+ const itemCheckboxInput = 'input#select-template-1';
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false);
+ await act(async () => {
+ wrapper
+ .find(itemCheckboxInput)
+ .closest('DataListCheck')
+ .props()
+ .onChange();
+ });
+ wrapper.update();
+ expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true);
+ });
+
+ test('should delete notifications', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ wrapper
+ .find('Checkbox#select-all')
+ .props()
+ .onChange(true);
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('button[aria-label="Delete"]').simulate('click');
+ wrapper.update();
+ });
+ const deleteButton = global.document.querySelector(
+ 'body div[role="dialog"] button[aria-label="confirm delete"]'
+ );
+ expect(deleteButton).not.toEqual(null);
+ await act(async () => {
+ deleteButton.click();
+ });
+ expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(3);
+ expect(OrganizationsAPI.read).toHaveBeenCalledTimes(2);
+ });
+
+ test('should show error dialog shown for failed deletion', async () => {
+ const itemCheckboxInput = 'input#select-template-1';
+ OrganizationsAPI.destroy.mockRejectedValue(
+ new Error({
+ response: {
+ config: {
+ method: 'delete',
+ url: '/api/v2/organizations/1',
+ },
+ data: 'An error occurred',
+ },
+ })
+ );
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper
+ .find(itemCheckboxInput)
+ .closest('DataListCheck')
+ .props()
+ .onChange();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('button[aria-label="Delete"]').simulate('click');
+ wrapper.update();
+ });
+ const deleteButton = global.document.querySelector(
+ 'body div[role="dialog"] button[aria-label="confirm delete"]'
+ );
+ expect(deleteButton).not.toEqual(null);
+ await act(async () => {
+ deleteButton.click();
+ });
+ wrapper.update();
+
+ const modal = wrapper.find('Modal');
+ expect(modal.prop('isOpen')).toEqual(true);
+ expect(modal.prop('title')).toEqual('Error!');
+ });
+
+ test('should show add button', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(wrapper.find('ToolbarAddButton').length).toBe(1);
+ });
+
+ test('should hide add button (rbac)', async () => {
+ OrganizationsAPI.readOptions.mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ },
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(wrapper.find('ToolbarAddButton').length).toBe(0);
+ });
+});
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
index ed26638ed6..0087e7f9a8 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx
@@ -34,7 +34,10 @@ function NotificationTemplateListItem({
onSelect,
i18n,
}) {
- const latestStatus = template.summary_fields?.recent_notifications[0]?.status;
+ const recentNotifications = template.summary_fields?.recent_notifications;
+ const latestStatus = recentNotifications
+ ? recentNotifications[0]?.status
+ : null;
const [status, setStatus] = useState(latestStatus);
useEffect(() => {
@@ -44,7 +47,7 @@ function NotificationTemplateListItem({
const { request: sendTestNotification, isLoading, error } = useRequest(
useCallback(() => {
NotificationTemplatesAPI.test(template.id);
- setStatus('pending');
+ setStatus('running');
}, [template.id])
);
@@ -72,11 +75,11 @@ function NotificationTemplateListItem({
{template.name}
,
-
+
{status && }
,
- {i18n._(t`Type`)}
+ {i18n._(t`Type:`)}{' '}
{NOTIFICATION_TYPES[template.notification_type] ||
template.notification_type}
,
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx
new file mode 100644
index 0000000000..5a4566779e
--- /dev/null
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import { NotificationTemplatesAPI } from '../../../api';
+import NotificationTemplateListItem from './NotificationTemplateListItem';
+
+jest.mock('../../../api/models/NotificationTemplates');
+
+const template = {
+ id: 3,
+ notification_type: 'slack',
+ name: 'Test Notification',
+ summary_fields: {
+ user_capabilities: {
+ edit: true,
+ },
+ recent_notifications: [
+ {
+ status: 'success',
+ },
+ ],
+ },
+};
+
+describe('', () => {
+ test('should render template row', () => {
+ const wrapper = mountWithContexts(
+
+ );
+
+ const cells = wrapper.find('DataListCell');
+ expect(cells).toHaveLength(3);
+ expect(cells.at(0).text()).toEqual('Test Notification');
+ expect(cells.at(1).text()).toEqual('Success');
+ expect(cells.at(2).text()).toEqual('Type: Slack');
+ });
+
+ test('should send test notification', async () => {
+ NotificationTemplatesAPI.test.mockResolvedValue({});
+
+ const wrapper = mountWithContexts(
+
+ );
+ await act(async () => {
+ wrapper
+ .find('Button')
+ .at(0)
+ .invoke('onClick')();
+ });
+ expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1);
+ expect(
+ wrapper
+ .find('DataListCell')
+ .at(1)
+ .text()
+ ).toEqual('Running');
+ });
+});
diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx
index 93babc8e06..9333850cf9 100644
--- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx
+++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx
@@ -1,18 +1,14 @@
import React from 'react';
-
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
-
import NotificationTemplates from './NotificationTemplates';
describe('', () => {
let pageWrapper;
let pageSections;
- let title;
beforeEach(() => {
pageWrapper = mountWithContexts();
pageSections = pageWrapper.find('PageSection');
- title = pageWrapper.find('Title');
});
afterEach(() => {
@@ -22,8 +18,6 @@ describe('', () => {
test('initially renders without crashing', () => {
expect(pageWrapper.length).toBe(1);
expect(pageSections.length).toBe(2);
- expect(title.length).toBe(1);
- expect(title.props().size).toBe('2xl');
expect(pageSections.first().props().variant).toBe('light');
});
});