From 4b62d77015bb3b7c634025ef455314a3899b8124 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 13 Dec 2019 10:05:17 -0500 Subject: [PATCH] Moves inventoryGroupForm into shared directory Updates InventoryGroups tests Adds ContentError functionalist to catch a case where a use might navigate to an Inventory that isn't associated to the shown inventoryGroup. --- .../Inventory/InventoryEdit/InventoryEdit.jsx | 8 +- .../InventoryGroup/InventoryGroup.jsx | 37 ++- .../InventoryGroup/InventoryGroup.test.jsx | 9 +- .../InventoryGroupAdd/InventoryGroupAdd.jsx | 2 +- .../InventoryGroupDetail.jsx | 12 +- .../InventoryGroupEdit/InventoryGroupEdit.jsx | 2 +- .../Inventory/InventoryGroupForm/index.js | 1 - .../InventoryGroups/InventoryGroups.test.jsx | 224 +++--------------- .../InventoryGroupsList.test.jsx | 217 +++++++++++++++++ .../InventoryGroupForm.jsx | 0 .../InventoryGroupForm.test.jsx | 0 11 files changed, 297 insertions(+), 215 deletions(-) delete mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx rename awx/ui_next/src/screens/Inventory/{InventoryGroupForm => shared}/InventoryGroupForm.jsx (100%) rename awx/ui_next/src/screens/Inventory/{InventoryGroupForm => shared}/InventoryGroupForm.test.jsx (100%) diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx index 131787ae95..2ec78aef4a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx @@ -15,7 +15,7 @@ import { getAddedAndRemoved } from '../../../util/lists'; function InventoryEdit({ history, i18n, inventory }) { const [error, setError] = useState(null); const [associatedInstanceGroups, setInstanceGroups] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [contentLoading, setContentLoading] = useState(true); const [credentialTypeId, setCredentialTypeId] = useState(null); useEffect(() => { @@ -39,11 +39,11 @@ function InventoryEdit({ history, i18n, inventory }) { } catch (err) { setError(err); } finally { - setIsLoading(false); + setContentLoading(false); } }; loadData(); - }, [inventory.id, isLoading, inventory, credentialTypeId]); + }, [inventory.id, contentLoading, inventory, credentialTypeId]); const handleCancel = () => { history.push('/inventories'); @@ -85,7 +85,7 @@ function InventoryEdit({ history, i18n, inventory }) { history.push(`${url}`); } }; - if (isLoading) { + if (contentLoading) { return ; } if (error) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx index db03a1ecf2..1c510f4fe6 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx @@ -14,8 +14,8 @@ import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail'; function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { const [inventoryGroup, setInventoryGroup] = useState(null); - const [hasContentLoading, setHasContentLoading] = useState(true); - const [contentError, setHasContentError] = useState(null); + const [contentLoading, setContentLoading] = useState(true); + const [contentError, setContentError] = useState(null); useEffect(() => { const loadData = async () => { @@ -24,9 +24,9 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { setInventoryGroup(data); setBreadcrumb(inventory, data); } catch (err) { - setHasContentError(err); + setContentError(err); } finally { - setHasContentLoading(false); + setContentLoading(false); } }; @@ -64,12 +64,32 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { id: 2, }, ]; + + // In cases where a user manipulates the url such that they try to navigate to a Inventory Group + // that is not associated with the Inventory Id in the Url this Content Error is thrown. + // Inventory Groups have a 1: 1 relationship to Inventories thus their Ids must corrolate. + + if (contentLoading) { + return ; + } + + if ( + inventoryGroup.summary_fields.inventory.id !== parseInt(match.params.id, 10) + ) { + return ( + + {inventoryGroup && ( + + {i18n._(t`View Inventory Groups`)} + + )} + + ); + } + if (contentError) { return ; } - if (hasContentLoading) { - return ; - } let cardHeader = null; if ( @@ -80,12 +100,11 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) { ); } - return ( <> {cardHeader} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx index 4c1b50c95b..6273de12d8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { GroupsAPI } from '@api'; +import { Route } from 'react-router-dom'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; @@ -14,6 +15,7 @@ GroupsAPI.readDetail.mockResolvedValue({ description: 'Bar', variables: 'bizz: buzz', summary_fields: { + inventory: { id: 1 }, created_by: { id: 1, name: 'Athena' }, modified_by: { id: 1, name: 'Apollo' }, }, @@ -29,7 +31,12 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - {}} />, + ( + {}} inventory={inventory} /> + )} + />, { context: { router: { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx index 720e9d4b3c..eff8a2fe10 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupAdd/InventoryGroupAdd.jsx @@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; import { Card } from '@patternfly/react-core'; -import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; +import InventoryGroupForm from '../shared/InventoryGroupForm'; function InventoryGroupsAdd({ history, inventory, setBreadcrumb }) { const [error, setError] = useState(null); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 28b662e2ee..8cf97cffc0 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -33,6 +33,9 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { summary_fields: { created_by, modified_by }, created, modified, + name, + description, + variables, } = inventoryGroup; const [error, setError] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -78,16 +81,13 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { return ( - - + + diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx index 6ff0e58c58..230314ce7c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupEdit/InventoryGroupEdit.jsx @@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react'; import { withRouter } from 'react-router-dom'; import { GroupsAPI } from '@api'; -import InventoryGroupForm from '../InventoryGroupForm/InventoryGroupForm'; +import InventoryGroupForm from '../shared/InventoryGroupForm'; function InventoryGroupEdit({ history, inventoryGroup, inventory, match }) { const [error, setError] = useState(null); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js b/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js deleted file mode 100644 index 090b2c2f8a..0000000000 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './InventoryGroupForm'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx index 8c60d8bfbd..d5a3665247 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx @@ -1,81 +1,25 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route } from 'react-router-dom'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; -import { InventoriesAPI, GroupsAPI } from '@api'; -import InventoryGroupsList from './InventoryGroupsList'; +import InventoryGroups from './InventoryGroups'; -jest.mock('@api'); - -const mockGroups = [ - { - id: 1, - type: 'group', - name: 'foo', - inventory: 1, - url: '/api/v2/groups/1', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - }, - }, - }, - { - id: 2, - type: 'group', - name: 'bar', - inventory: 1, - url: '/api/v2/groups/2', - summary_fields: { - user_capabilities: { - delete: true, - edit: true, - }, - }, - }, - { - id: 3, - type: 'group', - name: 'baz', - inventory: 1, - url: '/api/v2/groups/3', - summary_fields: { - user_capabilities: { - delete: false, - edit: false, - }, - }, - }, -]; - -describe('', () => { - let wrapper; - - beforeEach(async () => { - InventoriesAPI.readGroups.mockResolvedValue({ - data: { - count: mockGroups.length, - results: mockGroups, - }, - }); - InventoriesAPI.readGroupsOptions.mockResolvedValue({ - data: { - actions: { - GET: {}, - POST: {}, - }, - }, - }); +describe('', () => { + test('initially renders successfully', async () => { + let wrapper; const history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/3/groups'], + initialEntries: ['/inventories/inventory/1/groups'], }); + const inventory = { id: 1, name: 'Foo' }; + await act(async () => { wrapper = mountWithContexts( } + component={() => ( + {}} inventory={inventory} /> + )} />, { context: { @@ -84,134 +28,30 @@ describe('', () => { } ); }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - }); - - test('initially renders successfully', () => { + expect(wrapper.length).toBe(1); expect(wrapper.find('InventoryGroupsList').length).toBe(1); }); - - test('should fetch groups from api and render them in the list', async () => { - expect(InventoriesAPI.readGroups).toHaveBeenCalled(); - expect(wrapper.find('InventoryGroupItem').length).toBe(3); - }); - - test('should check and uncheck the row item', async () => { - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(false); - + test('test that InventoryGroupsAdd renders', async () => { + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/1/groups/add'], + }); + const inventory = { id: 1, name: 'Foo' }; + let wrapper; await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( - true - ); - }); - wrapper.update(); - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(true); - - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( - false - ); - }); - wrapper.update(); - expect( - wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked - ).toBe(false); - }); - - test('should check all row items when select all is checked', async () => { - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - await act(async () => { - wrapper.find('Checkbox#select-all').invoke('onChange')(true); - }); - wrapper.update(); - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(true); - }); - await act(async () => { - wrapper.find('Checkbox#select-all').invoke('onChange')(false); - }); - wrapper.update(); - wrapper.find('PFDataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - }); - - test('should show content error when api throws error on initial render', async () => { - InventoriesAPI.readGroupsOptions.mockImplementation(() => - Promise.reject(new Error()) - ); - await act(async () => { - wrapper = mountWithContexts(); - }); - await waitForElement(wrapper, 'ContentError', el => el.length === 1); - }); - - test('should show content error if groups are not successfully fetched from api', async () => { - InventoriesAPI.readGroups.mockImplementation(() => - Promise.reject(new Error()) - ); - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); - }); - await waitForElement( - wrapper, - 'InventoryGroupsDeleteModal', - el => el.props().isModalOpen === true - ); - await act(async () => { - wrapper - .find('ModalBoxFooter Button[aria-label="Delete"]') - .invoke('onClick')(); - }); - await waitForElement(wrapper, 'ContentError', el => el.length === 1); - }); - - test('should show error modal when group is not successfully deleted from api', async () => { - GroupsAPI.destroy.mockRejectedValue( - new Error({ - response: { - config: { - method: 'delete', - url: '/api/v2/groups/1', + wrapper = mountWithContexts( + ( + {}} inventory={inventory} /> + )} + />, + { + context: { + router: { history, route: { location: history.location } }, }, - data: 'An error occurred', - }, - }) - ); - await act(async () => { - wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); - }); - await waitForElement( - wrapper, - 'InventoryGroupsDeleteModal', - el => el.props().isModalOpen === true - ); - await act(async () => { - wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); - }); - wrapper.update(); - await act(async () => { - wrapper - .find('ModalBoxFooter Button[aria-label="Delete"]') - .invoke('onClick')(); - }); - await waitForElement(wrapper, { title: 'Error!', variant: 'danger' }); - await act(async () => { - wrapper.find('ModalBoxCloseButton').invoke('onClose')(); + } + ); }); + expect(wrapper.find('InventoryGroupsAdd').length).toBe(1); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx new file mode 100644 index 0000000000..8c60d8bfbd --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx @@ -0,0 +1,217 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { InventoriesAPI, GroupsAPI } from '@api'; +import InventoryGroupsList from './InventoryGroupsList'; + +jest.mock('@api'); + +const mockGroups = [ + { + id: 1, + type: 'group', + name: 'foo', + inventory: 1, + url: '/api/v2/groups/1', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + id: 2, + type: 'group', + name: 'bar', + inventory: 1, + url: '/api/v2/groups/2', + summary_fields: { + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + id: 3, + type: 'group', + name: 'baz', + inventory: 1, + url: '/api/v2/groups/3', + summary_fields: { + user_capabilities: { + delete: false, + edit: false, + }, + }, + }, +]; + +describe('', () => { + let wrapper; + + beforeEach(async () => { + InventoriesAPI.readGroups.mockResolvedValue({ + data: { + count: mockGroups.length, + results: mockGroups, + }, + }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/3/groups'], + }); + await act(async () => { + wrapper = mountWithContexts( + } + />, + { + context: { + router: { history, route: { location: history.location } }, + }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders successfully', () => { + expect(wrapper.find('InventoryGroupsList').length).toBe(1); + }); + + test('should fetch groups from api and render them in the list', async () => { + expect(InventoriesAPI.readGroups).toHaveBeenCalled(); + expect(wrapper.find('InventoryGroupItem').length).toBe(3); + }); + + test('should check and uncheck the row item', async () => { + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(false); + + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( + true + ); + }); + wrapper.update(); + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(true); + + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')( + false + ); + }); + wrapper.update(); + expect( + wrapper.find('PFDataListCheck[id="select-group-1"]').props().checked + ).toBe(false); + }); + + test('should check all row items when select all is checked', async () => { + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + await act(async () => { + wrapper.find('Checkbox#select-all').invoke('onChange')(true); + }); + wrapper.update(); + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(true); + }); + await act(async () => { + wrapper.find('Checkbox#select-all').invoke('onChange')(false); + }); + wrapper.update(); + wrapper.find('PFDataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + }); + + test('should show content error when api throws error on initial render', async () => { + InventoriesAPI.readGroupsOptions.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + + test('should show content error if groups are not successfully fetched from api', async () => { + InventoriesAPI.readGroups.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); + }); + await waitForElement( + wrapper, + 'InventoryGroupsDeleteModal', + el => el.props().isModalOpen === true + ); + await act(async () => { + wrapper + .find('ModalBoxFooter Button[aria-label="Delete"]') + .invoke('onClick')(); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + + test('should show error modal when group is not successfully deleted from api', async () => { + GroupsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/groups/1', + }, + data: 'An error occurred', + }, + }) + ); + await act(async () => { + wrapper.find('PFDataListCheck[id="select-group-1"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')(); + }); + await waitForElement( + wrapper, + 'InventoryGroupsDeleteModal', + el => el.props().isModalOpen === true + ); + await act(async () => { + wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); + }); + wrapper.update(); + await act(async () => { + wrapper + .find('ModalBoxFooter Button[aria-label="Delete"]') + .invoke('onClick')(); + }); + await waitForElement(wrapper, { title: 'Error!', variant: 'danger' }); + await act(async () => { + wrapper.find('ModalBoxCloseButton').invoke('onClose')(); + }); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.jsx rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx similarity index 100% rename from awx/ui_next/src/screens/Inventory/InventoryGroupForm/InventoryGroupForm.test.jsx rename to awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.test.jsx