Testing Improvements and Refactoring

This commit is contained in:
Alex Corey
2019-12-10 15:08:59 -05:00
parent f8a754cf44
commit 87a05a5b2e
11 changed files with 173 additions and 166 deletions

View File

@@ -14,8 +14,8 @@ import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
const [inventoryGroup, setInventoryGroup] = useState(null); const [inventoryGroup, setInventoryGroup] = useState(null);
const [hasContentLoading, setContentLoading] = useState(true); const [hasContentLoading, setHasContentLoading] = useState(true);
const [hasContentError, setHasContentError] = useState(false); const [contentError, setHasContentError] = useState(null);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -26,12 +26,18 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
} catch (err) { } catch (err) {
setHasContentError(err); setHasContentError(err);
} finally { } finally {
setContentLoading(false); setHasContentLoading(false);
} }
}; };
loadData(); loadData();
}, [match.params.groupId, setBreadcrumb, inventory]); }, [
history.location.pathname,
match.params.groupId,
inventory,
setBreadcrumb,
]);
const tabsArray = [ const tabsArray = [
{ {
name: i18n._(t`Return to Groups`), name: i18n._(t`Return to Groups`),
@@ -46,7 +52,7 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
id: 0, id: 0,
}, },
{ {
name: i18n._(t`RelatedGroups`), name: i18n._(t`Related Groups`),
link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup && link: `/inventories/inventory/${inventory.id}/groups/${inventoryGroup &&
inventoryGroup.id}/nested_groups`, inventoryGroup.id}/nested_groups`,
id: 1, id: 1,
@@ -58,26 +64,28 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
id: 2, id: 2,
}, },
]; ];
if (hasContentError) { if (contentError) {
return <ContentError />; return <ContentError error={contentError} />;
} }
if (hasContentLoading) { if (hasContentLoading) {
return <ContentLoading />; return <ContentLoading />;
} }
let cardHeader = hasContentLoading ? null : (
<CardHeader style={{ padding: 0 }}> let cardHeader = null;
<RoutedTabs history={history} tabsArray={tabsArray} />
<CardCloseButton
linkTo={`/inventories/inventory/${inventory.id}/group`}
/>
</CardHeader>
);
if ( if (
!history.location.pathname.includes('groups/') || history.location.pathname.includes('groups/') &&
history.location.pathname.endsWith('edit') !history.location.pathname.endsWith('edit')
) { ) {
cardHeader = null; cardHeader = (
<CardHeader style={{ padding: 0 }}>
<RoutedTabs history={history} tabsArray={tabsArray} />
<CardCloseButton
linkTo={`/inventories/inventory/${inventory.id}/group`}
/>
</CardHeader>
);
} }
return ( return (
<> <>
{cardHeader} {cardHeader}

View File

@@ -45,26 +45,20 @@ describe('<InventoryGroup />', () => {
} }
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
afterEach(() => { afterEach(() => {
wrapper.unmount(); wrapper.unmount();
}); });
test('renders successfully', async () => { test('renders successfully', async () => {
await act(async () => {
waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe(
1
);
}); });
test('expect Return to Groups tab to exist', async () => { test('expect all tabs to exist, including Return to Groups', async () => {
await act(async () => {
waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
expect(wrapper.length).toBe(1);
expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe( expect(wrapper.find('button[aria-label="Return to Groups"]').length).toBe(
1 1
); );
expect(wrapper.find('button[aria-label="Details"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1);
}); });
}); });

View File

@@ -7,8 +7,10 @@ import { Card } from '@patternfly/react-core';
import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm';
function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => setBreadcrumb(inventory), [inventory, setBreadcrumb]);
const handleSubmit = async values => { const handleSubmit = async values => {
values.inventory = inventory.id; values.inventory = inventory.id;
try { try {
@@ -18,9 +20,11 @@ function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) {
setError(err); setError(err);
} }
}; };
const handleCancel = () => { const handleCancel = () => {
history.push(`/inventories/inventory/${inventory.id}/groups`); history.push(`/inventories/inventory/${inventory.id}/groups`);
}; };
return ( return (
<Card> <Card>
<InventoryGroupForm <InventoryGroupForm

View File

@@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom';
import { GroupsAPI } from '@api'; import { GroupsAPI } from '@api';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import InventoryGroupAdd from './InventoryGroupAdd'; import InventoryGroupAdd from './InventoryGroupAdd';
@@ -13,19 +15,19 @@ describe('<InventoryGroupAdd />', () => {
let history; let history;
beforeEach(async () => { beforeEach(async () => {
history = createMemoryHistory({ history = createMemoryHistory({
initialEntries: ['/inventories/inventory/1/groups'], initialEntries: ['/inventories/inventory/1/groups/add'],
}); });
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventoryGroupAdd <Route
setBreadcrumb={() => {}} path="/inventories/inventory/:id/groups/add"
inventory={{ inventory: { id: 1 } }} component={() => (
<InventoryGroupAdd setBreadcrumb={() => {}} inventory={{ id: 1 }} />
)}
/>, />,
{ {
context: { context: {
router: { router: { history, route: { location: history.location } },
history,
},
}, },
} }
); );
@@ -38,17 +40,12 @@ describe('<InventoryGroupAdd />', () => {
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
}); });
test('cancel should navigate user to Inventory Groups List', async () => { test('cancel should navigate user to Inventory Groups List', async () => {
await act(async () => { wrapper.find('button[aria-label="Cancel"]').simulate('click');
waitForElement(wrapper, 'isLoading', el => el.length === 0);
});
expect(history.location.pathname).toEqual( expect(history.location.pathname).toEqual(
'/inventories/inventory/1/groups' '/inventories/inventory/1/groups'
); );
}); });
test('handleSubmit should call api', async () => { test('handleSubmit should call api', async () => {
await act(async () => {
waitForElement(wrapper, 'isLoading', el => el.length === 0);
});
await act(async () => { await act(async () => {
wrapper.find('InventoryGroupForm').prop('handleSubmit')({ wrapper.find('InventoryGroupForm').prop('handleSubmit')({
name: 'Bar', name: 'Bar',

View File

@@ -29,6 +29,9 @@ const ActionButtonWrapper = styled.div`
} }
`; `;
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
const {
summary_fields: { created_by, modified_by },
} = inventoryGroup;
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -41,52 +44,7 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
setError(err); setError(err);
} }
}; };
if (error) {
return (
<AlertModal
variant="danger"
title={i18n._(t`Error!`)}
isOpen={error}
onClose={() => setError(false)}
>
{i18n._(t`Failed to delete group ${inventoryGroup.name}.`)}
<ErrorDetail error={error} />
</AlertModal>
);
}
if (isDeleteModalOpen) {
return (
<AlertModal
variant="danger"
title={i18n._(t`Delete Inventory Group`)}
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={() => setIsDeleteModalOpen(false)}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
{i18n._(t`Are you sure you want to delete:`)}
<br />
<strong>{inventoryGroup.name}</strong>
<br />
</AlertModal>
);
}
return ( return (
<CardBody style={{ paddingTop: '20px' }}> <CardBody style={{ paddingTop: '20px' }}>
<DetailList gutter="sm"> <DetailList gutter="sm">
@@ -104,32 +62,32 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
label={i18n._(t`Variables`)} label={i18n._(t`Variables`)}
/> />
<DetailList> <DetailList>
<Detail {created_by && created_by.username && (
label={i18n._(t`Created`)} <Detail
value={ label={i18n._(t`Created`)}
<span> value={
{i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} <span>
<Link {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '}
to={`/users/${inventoryGroup.summary_fields.created_by.id}`} <Link to={`/users/${created_by.id}`}>
> {created_by.username}
{inventoryGroup.summary_fields.created_by.username} </Link>
</Link> </span>
</span> }
} />
/> )}
<Detail {modified_by && modified_by.username && (
label={i18n._(t`Modified`)} <Detail
value={ label={i18n._(t`Modified`)}
<span> value={
{i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} <span>
<Link {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '}
to={`/users/${inventoryGroup.summary_fields.modified_by.id}`} <Link to={`/users/${modified_by.id}`}>
> {modified_by.username}
{inventoryGroup.summary_fields.modified_by.username} </Link>
</Link> </span>
</span> }
} />
/> )}
</DetailList> </DetailList>
<ActionButtonWrapper> <ActionButtonWrapper>
<Button <Button
@@ -151,6 +109,48 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</Button> </Button>
</ActionButtonWrapper> </ActionButtonWrapper>
{isDeleteModalOpen && (
<AlertModal
variant="danger"
title={i18n._(t`Delete Inventory Group`)}
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={() => setIsDeleteModalOpen(false)}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
{i18n._(t`Are you sure you want to delete:`)}
<br />
<strong>{inventoryGroup.name}</strong>
<br />
</AlertModal>
)}
{error && (
<AlertModal
variant="danger"
title={i18n._(t`Error!`)}
isOpen={error}
onClose={() => setError(false)}
>
{i18n._(t`Failed to delete group ${inventoryGroup.name}.`)}
<ErrorDetail error={error} />
</AlertModal>
)}
</CardBody> </CardBody>
); );
} }

View File

@@ -33,7 +33,7 @@ describe('<InventoryGroupDetail />', () => {
beforeEach(async () => { beforeEach(async () => {
await act(async () => { await act(async () => {
history = createMemoryHistory({ history = createMemoryHistory({
initialEntries: ['/inventories/inventory/1/groups/1/edit'], initialEntries: ['/inventories/inventory/1/groups/1/details'],
}); });
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Route <Route
@@ -69,7 +69,7 @@ describe('<InventoryGroupDetail />', () => {
expect(GroupsAPI.destroy).toBeCalledWith(1); expect(GroupsAPI.destroy).toBeCalledWith(1);
}); });
test('should navigate user to edit form on edit button click', async () => { test('should navigate user to edit form on edit button click', async () => {
wrapper.find('button[aria-label="Edit"]').prop('onClick'); wrapper.find('button[aria-label="Edit"]').simulate('click');
expect(history.location.pathname).toEqual( expect(history.location.pathname).toEqual(
'/inventories/inventory/1/groups/1/edit' '/inventories/inventory/1/groups/1/edit'
); );

View File

@@ -11,19 +11,20 @@ function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) {
const handleSubmit = async values => { const handleSubmit = async values => {
try { try {
await GroupsAPI.update(match.params.groupId, values); await GroupsAPI.update(match.params.groupId, values);
history.push(
`/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}`
);
} catch (err) { } catch (err) {
setError(err); setError(err);
} finally {
history.push(
`/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details`
);
} }
}; };
const handleCancel = () => { const handleCancel = () => {
history.push( history.push(
`/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}/details` `/inventories/inventory/${inventory.id}/groups/${inventoryGroup.id}`
); );
}; };
return ( return (
<InventoryGroupForm <InventoryGroupForm
error={error} error={error}

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { Route } from 'react-router-dom';
import { GroupsAPI } from '@api'; import { GroupsAPI } from '@api';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import InventoryGroupEdit from './InventoryGroupEdit'; import InventoryGroupEdit from './InventoryGroupEdit';
@@ -19,13 +20,19 @@ describe('<InventoryGroupEdit />', () => {
let history; let history;
beforeEach(async () => { beforeEach(async () => {
history = createMemoryHistory({ history = createMemoryHistory({
initialEntries: ['/inventories/1/groups'], initialEntries: ['/inventories/inventory/1/groups/2/edit'],
}); });
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventoryGroupEdit <Route
setBreadcrumb={jest.fn()} path="/inventories/inventory/:id/groups/:groupId/edit"
inventory={{ inventory: { id: 1 } }} component={() => (
<InventoryGroupEdit
setBreadcrumb={() => {}}
inventory={{ id: 1 }}
inventoryGroup={{ id: 2 }}
/>
)}
/>, />,
{ {
context: { context: {
@@ -35,6 +42,7 @@ describe('<InventoryGroupEdit />', () => {
match: { match: {
params: { groupId: 13 }, params: { groupId: 13 },
}, },
location: history.location,
}, },
}, },
}, },
@@ -49,11 +57,12 @@ describe('<InventoryGroupEdit />', () => {
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
}); });
test('cancel should navigate user to Inventory Groups List', async () => { test('cancel should navigate user to Inventory Groups List', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0); wrapper.find('button[aria-label="Cancel"]').simulate('click');
expect(history.location.pathname).toEqual('/inventories/1/groups'); expect(history.location.pathname).toEqual(
'/inventories/inventory/1/groups/2'
);
}); });
test('handleSubmit should call api', async () => { test('handleSubmit should call api', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
wrapper.find('InventoryGroupForm').prop('handleSubmit')({ wrapper.find('InventoryGroupForm').prop('handleSubmit')({
name: 'Bar', name: 'Bar',
description: 'Ansible', description: 'Ansible',

View File

@@ -26,7 +26,6 @@ describe('<InventoryGroupForm />', () => {
expect(wrapper.length).toBe(1); expect(wrapper.length).toBe(1);
}); });
test('should render values for the fields that have them', () => { test('should render values for the fields that have them', () => {
expect(wrapper.length).toBe(1);
expect(wrapper.find("FormGroup[label='Name']").length).toBe(1); expect(wrapper.find("FormGroup[label='Name']").length).toBe(1);
expect(wrapper.find("FormGroup[label='Description']").length).toBe(1); expect(wrapper.find("FormGroup[label='Description']").length).toBe(1);
expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1); expect(wrapper.find("VariablesField[label='Variables']").length).toBe(1);

View File

@@ -11,37 +11,32 @@ import InventoryGroupsList from './InventoryGroupsList';
function InventoryGroups({ setBreadcrumb, inventory, location, match }) { function InventoryGroups({ setBreadcrumb, inventory, location, match }) {
return ( return (
<Switch> <Switch>
{[ <Route
<Route key="add"
key="list" path="/inventories/inventory/:id/groups/add"
path="/inventories/inventory/:id/groups" render={() => {
render={() => { return (
return <InventoryGroupsList location={location} match={match} />; <InventoryGroupAdd
}}
/>,
<Route
key="add"
path="/inventories/inventory/:id/groups/add"
render={() => {
return (
<InventoryGroupAdd
setBreadcrumb={setBreadcrumb}
inventory={inventory}
/>
);
}}
/>,
<Route
key="details"
path="/inventories/inventory/:id/groups/:groupId/"
render={() => (
<InventoryGroup
inventory={inventory}
setBreadcrumb={setBreadcrumb} setBreadcrumb={setBreadcrumb}
inventory={inventory}
/> />
)} );
/>, }}
]} />
<Route
key="details"
path="/inventories/inventory/:id/groups/:groupId/"
render={() => (
<InventoryGroup inventory={inventory} setBreadcrumb={setBreadcrumb} />
)}
/>
<Route
key="list"
path="/inventories/inventory/:id/groups"
render={() => {
return <InventoryGroupsList location={location} match={match} />;
}}
/>
</Switch> </Switch>
); );
} }

View File

@@ -4,7 +4,7 @@ import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { InventoriesAPI, GroupsAPI } from '@api'; import { InventoriesAPI, GroupsAPI } from '@api';
import InventoryGroups from './InventoryGroups'; import InventoryGroupsList from './InventoryGroupsList';
jest.mock('@api'); jest.mock('@api');
@@ -50,7 +50,7 @@ const mockGroups = [
}, },
]; ];
describe('<InventoryGroups />', () => { describe('<InventoryGroupsList />', () => {
let wrapper; let wrapper;
beforeEach(async () => { beforeEach(async () => {
@@ -75,7 +75,7 @@ describe('<InventoryGroups />', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<Route <Route
path="/inventories/inventory/:id/groups" path="/inventories/inventory/:id/groups"
component={() => <InventoryGroups />} component={() => <InventoryGroupsList />}
/>, />,
{ {
context: { context: {
@@ -88,7 +88,7 @@ describe('<InventoryGroups />', () => {
}); });
test('initially renders successfully', () => { test('initially renders successfully', () => {
expect(wrapper.find('InventoryGroups').length).toBe(1); expect(wrapper.find('InventoryGroupsList').length).toBe(1);
}); });
test('should fetch groups from api and render them in the list', async () => { test('should fetch groups from api and render them in the list', async () => {
@@ -147,7 +147,7 @@ describe('<InventoryGroups />', () => {
Promise.reject(new Error()) Promise.reject(new Error())
); );
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventoryGroups />); wrapper = mountWithContexts(<InventoryGroupsList />);
}); });
await waitForElement(wrapper, 'ContentError', el => el.length === 1); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
}); });