diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx index 6478856675..2a0e419675 100644 --- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx +++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx @@ -30,7 +30,7 @@ describe('', () => { }); test('handleSubmit should post to api', async () => { - act(() => { + await act(async () => { wrapper.find('HostForm').prop('handleSubmit')(hostData); }); expect(HostsAPI.create).toHaveBeenCalledWith(hostData); @@ -44,12 +44,14 @@ describe('', () => { test('successful form submission should trigger redirect', async () => { HostsAPI.create.mockResolvedValueOnce({ data: { - id: 5, ...hostData, + id: 5, }, }); await waitForElement(wrapper, 'button[aria-label="Save"]'); - await wrapper.find('HostForm').invoke('handleSubmit')(hostData); + await act(async () => { + wrapper.find('HostForm').invoke('handleSubmit')(hostData); + }); expect(history.location.pathname).toEqual('/hosts/5/details'); }); }); diff --git a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx index 7dd3e6b978..90f994aa6b 100644 --- a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx +++ b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx @@ -1,17 +1,40 @@ -import React from 'react'; -import { Link, useParams } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Button } from '@patternfly/react-core'; +import AlertModal from '@components/AlertModal'; import { CardBody, CardActionsRow } from '@components/Card'; +import ContentLoading from '@components/ContentLoading'; +import DeleteButton from '@components/DeleteButton'; import { DetailList, Detail } from '@components/DetailList'; +import ErrorDetail from '@components/ErrorDetail'; import { formatDateString } from '@util/dates'; +import { TeamsAPI } from '@api'; function TeamDetail({ team, i18n }) { const { name, description, created, modified, summary_fields } = team; + const [deletionError, setDeletionError] = useState(null); + const [hasContentLoading, setHasContentLoading] = useState(false); + const history = useHistory(); const { id } = useParams(); + const handleDelete = async () => { + setHasContentLoading(true); + try { + await TeamsAPI.destroy(id); + history.push(`/teams`); + } catch (error) { + setDeletionError(error); + } + setHasContentLoading(false); + }; + + if (hasContentLoading) { + return ; + } + return ( @@ -36,12 +59,38 @@ function TeamDetail({ team, i18n }) { /> - {summary_fields.user_capabilities.edit && ( - - )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.edit && ( + + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + {deletionError && ( + setDeletionError(null)} + > + {i18n._(t`Failed to delete team.`)} + + + )} ); } diff --git a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx index 5b971c454c..420985a307 100644 --- a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx +++ b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx @@ -1,12 +1,13 @@ import React from 'react'; - +import { act } from 'react-dom/test-utils'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; - import TeamDetail from './TeamDetail'; +import { TeamsAPI } from '@api'; jest.mock('@api'); describe('', () => { + let wrapper; const mockTeam = { name: 'Foo', description: 'Bar', @@ -19,15 +20,25 @@ describe('', () => { }, user_capabilities: { edit: true, + delete: true, }, }, }; - test('initially renders succesfully', () => { - mountWithContexts(); + + beforeEach(async () => { + wrapper = mountWithContexts(); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); - test('should render Details', async done => { - const wrapper = mountWithContexts(); + afterEach(() => { + wrapper.unmount(); + }); + + test('initially renders succesfully', async () => { + await waitForElement(wrapper, 'TeamDetail'); + }); + + test('should render Details', async () => { const testParams = [ { label: 'Name', value: 'Foo' }, { label: 'Description', value: 'Bar' }, @@ -42,23 +53,52 @@ describe('', () => { expect(detail.find('dt').text()).toBe(label); expect(detail.find('dd').text()).toBe(value); } - done(); }); - test('should show edit button for users with edit permission', async done => { - const wrapper = mountWithContexts(); - const editButton = await waitForElement(wrapper, 'TeamDetail Button'); + test('should show edit button for users with edit permission', async () => { + const editButton = await waitForElement( + wrapper, + 'TeamDetail Button[aria-label="Edit"]' + ); expect(editButton.text()).toEqual('Edit'); expect(editButton.prop('to')).toBe('/teams/undefined/edit'); - done(); }); - test('should hide edit button for users without edit permission', async done => { + test('should hide edit button for users without edit permission', async () => { const readOnlyTeam = { ...mockTeam }; readOnlyTeam.summary_fields.user_capabilities.edit = false; - const wrapper = mountWithContexts(); + wrapper = mountWithContexts(); await waitForElement(wrapper, 'TeamDetail'); - expect(wrapper.find('TeamDetail Button').length).toBe(0); - done(); + expect(wrapper.find('TeamDetail Button[aria-label="Edit"]').length).toBe(0); + }); + + test('expected api call is made for delete', async () => { + await waitForElement(wrapper, 'TeamDetail Button[aria-label="Delete"]'); + await act(async () => { + wrapper.find('DeleteButton').invoke('onConfirm')(); + }); + expect(TeamsAPI.destroy).toHaveBeenCalledTimes(1); + }); + + test('Error dialog shown for failed deletion', async () => { + TeamsAPI.destroy.mockImplementationOnce(() => Promise.reject(new Error())); + wrapper = mountWithContexts(); + await waitForElement(wrapper, 'TeamDetail Button[aria-label="Delete"]'); + await act(async () => { + wrapper.find('DeleteButton').invoke('onConfirm')(); + }); + await waitForElement( + wrapper, + 'Modal[title="Error!"]', + el => el.length === 1 + ); + await act(async () => { + wrapper.find('Modal[title="Error!"]').invoke('onClose')(); + }); + await waitForElement( + wrapper, + 'Modal[title="Error!"]', + el => el.length === 0 + ); }); }); diff --git a/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx b/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx index 840a39ed1d..72c4bdab6a 100644 --- a/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx +++ b/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx @@ -1,12 +1,17 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; +import AlertModal from '@components/AlertModal'; +import { Button } from '@patternfly/react-core'; import { CardBody, CardActionsRow } from '@components/Card'; +import ContentLoading from '@components/ContentLoading'; +import DeleteButton from '@components/DeleteButton'; import { DetailList, Detail } from '@components/DetailList'; +import ErrorDetail from '@components/ErrorDetail'; import { formatDateString } from '@util/dates'; +import { UsersAPI } from '@api'; function UserDetail({ user, i18n }) { const { @@ -21,6 +26,20 @@ function UserDetail({ user, i18n }) { is_system_auditor, summary_fields, } = user; + const [deletionError, setDeletionError] = useState(null); + const [hasContentLoading, setHasContentLoading] = useState(false); + const history = useHistory(); + + const handleDelete = async () => { + setHasContentLoading(true); + try { + await UsersAPI.destroy(id); + history.push(`/users`); + } catch (error) { + setDeletionError(error); + } + setHasContentLoading(false); + }; let user_type; if (is_superuser) { @@ -31,6 +50,10 @@ function UserDetail({ user, i18n }) { user_type = i18n._(t`Normal User`); } + if (hasContentLoading) { + return ; + } + return ( @@ -52,16 +75,38 @@ function UserDetail({ user, i18n }) { - {summary_fields.user_capabilities.edit && ( - - )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.edit && ( + + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + {deletionError && ( + setDeletionError(null)} + > + {i18n._(t`Failed to delete user.`)} + + + )} ); } diff --git a/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx b/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx index 7e3b379e48..69ec4f36e7 100644 --- a/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx +++ b/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx @@ -1,9 +1,13 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { UsersAPI } from '@api'; import UserDetail from './UserDetail'; import mockDetails from '../data.user.json'; +jest.mock('@api'); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -39,7 +43,36 @@ describe('', () => { assertDetail('Created', `10/28/2019, 3:01:07 PM`); }); - test('should show edit button for users with edit permission', async done => { + test('User Type Detail should render expected strings', async () => { + let wrapper; + wrapper = mountWithContexts( + + ); + expect(wrapper.find(`Detail[label="User Type"] dd`).text()).toBe( + 'System Auditor' + ); + + wrapper = mountWithContexts( + + ); + expect(wrapper.find(`Detail[label="User Type"] dd`).text()).toBe( + 'Normal User' + ); + }); + + test('should show edit button for users with edit permission', async () => { const wrapper = mountWithContexts(); const editButton = await waitForElement( wrapper, @@ -47,10 +80,9 @@ describe('', () => { ); expect(editButton.text()).toEqual('Edit'); expect(editButton.prop('to')).toBe(`/users/${mockDetails.id}/edit`); - done(); }); - test('should hide edit button for users without edit permission', async done => { + test('should hide edit button for users without edit permission', async () => { const wrapper = mountWithContexts( ', () => { ); await waitForElement(wrapper, 'UserDetail'); expect(wrapper.find('UserDetail Button[aria-label="edit"]').length).toBe(0); - done(); }); test('edit button should navigate to user edit', () => { @@ -79,4 +110,35 @@ describe('', () => { .simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/users/1/edit'); }); + + test('expected api call is made for delete', async () => { + const wrapper = mountWithContexts(); + await waitForElement(wrapper, 'UserDetail Button[aria-label="Delete"]'); + await act(async () => { + wrapper.find('DeleteButton').invoke('onConfirm')(); + }); + expect(UsersAPI.destroy).toHaveBeenCalledTimes(1); + }); + + test('Error dialog shown for failed deletion', async () => { + UsersAPI.destroy.mockImplementationOnce(() => Promise.reject(new Error())); + const wrapper = mountWithContexts(); + await waitForElement(wrapper, 'UserDetail Button[aria-label="Delete"]'); + await act(async () => { + wrapper.find('DeleteButton').invoke('onConfirm')(); + }); + await waitForElement( + wrapper, + 'Modal[title="Error!"]', + el => el.length === 1 + ); + await act(async () => { + wrapper.find('Modal[title="Error!"]').invoke('onClose')(); + }); + await waitForElement( + wrapper, + 'Modal[title="Error!"]', + el => el.length === 0 + ); + }); }); diff --git a/awx/ui_next/src/screens/User/data.user.json b/awx/ui_next/src/screens/User/data.user.json index 3ab136a9b6..fc71d1f128 100644 --- a/awx/ui_next/src/screens/User/data.user.json +++ b/awx/ui_next/src/screens/User/data.user.json @@ -18,7 +18,7 @@ "summary_fields": { "user_capabilities": { "edit": true, - "delete": false + "delete": true } }, "created": "2019-10-28T15:01:07.218634Z",