diff --git a/awx/ui_next/jest.setup.js b/awx/ui_next/jest.setup.js index 62cb566c00..6dd8eff3bc 100644 --- a/awx/ui_next/jest.setup.js +++ b/awx/ui_next/jest.setup.js @@ -6,4 +6,6 @@ export const asyncFlush = () => new Promise((resolve) => setImmediate(resolve)); const enzyme = require('enzyme'); const Adapter = require('enzyme-adapter-react-16'); +jest.setTimeout(5000 * 4); + enzyme.configure({ adapter: new Adapter() }); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx similarity index 57% rename from awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.jsx rename to awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index ff96a0b6c0..288a30733f 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -1,11 +1,10 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Link, withRouter } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; import { Dropdown, DropdownPosition } from '@patternfly/react-core'; import { ToolbarAddButton } from '@components/PaginatedDataList'; -function TemplateAddButton({ match, i18n }) { +function AddDropDownButton({ dropdownItems }) { const [isOpen, setIsOpen] = useState(false); const element = useRef(null); @@ -29,26 +28,28 @@ function TemplateAddButton({ match, i18n }) { isOpen={isOpen} position={DropdownPosition.right} toggle={ setIsOpen(!isOpen)} />} - dropdownItems={[ + dropdownItems={dropdownItems.map(item => ( - {i18n._(t`Job Template`)} - , - - {i18n._(t`Workflow Template`)} - , - ]} + {item.label} + + ))} /> ); } -export { TemplateAddButton as _TemplateAddButton }; -export default withI18n()(withRouter(TemplateAddButton)); +AddDropDownButton.propTypes = { + dropdownItems: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }) + ).isRequired, +}; + +export { AddDropDownButton as _AddDropDownButton }; +export default AddDropDownButton; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.test.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.test.jsx similarity index 52% rename from awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.test.jsx rename to awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.test.jsx index 1a0e75e29e..a30e3f9cd8 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateAddButton.test.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.test.jsx @@ -1,22 +1,35 @@ import React from 'react'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; -import TemplateAddButton from './TemplateAddButton'; +import AddDropDownButton from './AddDropDownButton'; -describe('', () => { +describe('', () => { + const dropdownItems = [ + { + key: 'inventory', + label: 'Inventory', + url: `inventory/inventory/add/`, + }, + ]; test('should be closed initially', () => { - const wrapper = mountWithContexts(); + const wrapper = mountWithContexts( + + ); expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(false); }); test('should render two links', () => { - const wrapper = mountWithContexts(); + const wrapper = mountWithContexts( + + ); wrapper.find('button').simulate('click'); expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true); - expect(wrapper.find('Link')).toHaveLength(2); + expect(wrapper.find('Link')).toHaveLength(dropdownItems.length); }); test('should close when button re-clicked', () => { - const wrapper = mountWithContexts(); + const wrapper = mountWithContexts( + + ); wrapper.find('button').simulate('click'); expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true); wrapper.find('button').simulate('click'); diff --git a/awx/ui_next/src/components/AddDropDownButton/index.js b/awx/ui_next/src/components/AddDropDownButton/index.js new file mode 100644 index 0000000000..8c87c7df31 --- /dev/null +++ b/awx/ui_next/src/components/AddDropDownButton/index.js @@ -0,0 +1 @@ +export { default } from './AddDropDownButton'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index c949b39656..59e63a9b35 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -1,15 +1,9 @@ import React, { Component } from 'react'; -import { withRouter, Link } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Card, - PageSection, - Dropdown, - DropdownItem, - DropdownPosition, -} from '@patternfly/react-core'; +import { Card, PageSection } from '@patternfly/react-core'; import { InventoriesAPI } from '@api'; import AlertModal from '@components/AlertModal'; @@ -17,10 +11,10 @@ import DatalistToolbar from '@components/DataListToolbar'; import ErrorDetail from '@components/ErrorDetail'; import PaginatedDataList, { ToolbarDeleteButton, - ToolbarAddButton, } from '@components/PaginatedDataList'; -import { getQSConfig, parseQueryString } from '@util/qs'; +import { getQSConfig, parseQueryString } from '@util/qs'; +import AddDropDownButton from '@components/AddDropDownButton'; import InventoryListItem from './InventoryListItem'; // The type value in const QS_CONFIG below does not have a space between job_inventory and @@ -42,7 +36,6 @@ class InventoriesList extends Component { selected: [], inventories: [], itemCount: 0, - isAddOpen: false, }; this.loadInventories = this.loadInventories.bind(this); @@ -50,7 +43,6 @@ class InventoriesList extends Component { this.handleSelect = this.handleSelect.bind(this); this.handleInventoryDelete = this.handleInventoryDelete.bind(this); this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); - this.handleAddToggle = this.handleAddToggle.bind(this); } componentDidMount() { @@ -65,10 +57,6 @@ class InventoriesList extends Component { } } - componentWillUnmount() { - document.removeEventListener('click', this.handleAddToggle, false); - } - handleDeleteErrorClose() { this.setState({ deletionError: null }); } @@ -88,21 +76,6 @@ class InventoriesList extends Component { } } - handleAddToggle(e) { - const { isAddOpen } = this.state; - document.addEventListener('click', this.handleAddToggle, false); - - if (this.node && this.node.contains(e.target) && isAddOpen) { - document.removeEventListener('click', this.handleAddToggle, false); - this.setState({ isAddOpen: false }); - } else if (this.node && this.node.contains(e.target) && !isAddOpen) { - this.setState({ isAddOpen: true }); - } else { - this.setState({ isAddOpen: false }); - document.removeEventListener('click', this.handleAddToggle, false); - } - } - async handleInventoryDelete() { const { selected, itemCount } = this.state; @@ -168,14 +141,27 @@ class InventoriesList extends Component { inventories, itemCount, selected, - isAddOpen, actions, } = this.state; const { match, i18n } = this.props; const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const isAllSelected = - selected.length > 0 && selected.length === inventories.length; + const isAllSelected = selected.length === inventories.length; + const addButton = ( + + ); return ( @@ -221,35 +207,7 @@ class InventoriesList extends Component { itemsToDelete={selected} pluralizedItemName="Inventories" />, - canAdd && ( -
{ - this.node = node; - }} - key="add" - > - - } - dropdownItems={[ - - - {i18n._(t`Inventory`)} - - , - - - {i18n._(t`Smart Inventory`)} - - , - ]} - /> -
- ), + canAdd && addButton, ]} /> )} @@ -267,35 +225,7 @@ class InventoriesList extends Component { isSelected={selected.some(row => row.id === inventory.id)} /> )} - emptyStateControls={ - canAdd && ( -
{ - this.node = node; - }} - key="add" - > - } - dropdownItems={[ - - - {i18n._(t`Inventory`)} - - , - - - {i18n._(t`Smart Inventory`)} - - , - ]} - /> -
- ) - } + emptyStateControls={canAdd && addButton} />
', () => { done(); }); - test('Add button shown for users without ability to POST', async done => { + test('Add button shown for users with ability to POST', async done => { const wrapper = mountWithContexts(); await waitForElement( wrapper, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx index bfb4817941..584287444e 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx @@ -26,7 +26,7 @@ describe('', () => { id: 100, credential_type_id: 4, kind: 'scm', - name: 'alpha', + name: 'Foo', }, }, }; diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index eb9a049e41..fb8f1435a8 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -200,17 +200,22 @@ describe('', () => { data: { ...updatedTemplateData }, }); const formik = wrapper.find('Formik').instance(); - const changeState = new Promise(resolve => { - const values = { - ...mockJobTemplate, - ...updatedTemplateData, - labels, - instanceGroups: [], - }; - formik.setState({ values }, () => resolve()); - }); + const changeState = await act( + () => + new Promise(resolve => { + const values = { + ...mockJobTemplate, + ...updatedTemplateData, + labels, + instanceGroups: [], + }; + formik.setState({ values }, () => resolve()); + }) + ); await changeState; - wrapper.find('button[aria-label="Save"]').simulate('click'); + await act(async () => { + wrapper.find('button[aria-label="Save"]').simulate('click'); + }); await sleep(0); expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, { @@ -236,7 +241,9 @@ describe('', () => { 'button[aria-label="Cancel"]', e => e.length === 1 ); - cancelButton.prop('onClick')(); + await act(async () => { + cancelButton.prop('onClick')(); + }); expect(history.location.pathname).toEqual( '/templates/job_template/1/details' ); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 68e2bb7400..7a087f88fe 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -13,8 +13,8 @@ import PaginatedDataList, { } from '@components/PaginatedDataList'; import { getQSConfig, parseQueryString } from '@util/qs'; +import AddDropDownButton from '@components/AddDropDownButton'; import TemplateListItem from './TemplateListItem'; -import TemplateAddButton from './TemplateAddButton'; // The type value in const QS_CONFIG below does not have a space between job_template and // workflow_job_template so the params sent to the API match what the api expects. @@ -161,6 +161,21 @@ class TemplatesList extends Component { actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const isAllSelected = selected.length === templates.length && selected.length > 0; + const addButton = ( + + ); return ( @@ -206,7 +221,7 @@ class TemplatesList extends Component { itemsToDelete={selected} pluralizedItemName="Templates" />, - canAdd && , + canAdd && addButton, ]} /> )} @@ -220,7 +235,7 @@ class TemplatesList extends Component { isSelected={selected.some(row => row.id === template.id)} /> )} - emptyStateControls={canAdd && } + emptyStateControls={canAdd && addButton} /> ', () => { target: { value: 'new baz type', name: 'playbook' }, }); expect(form.state('values').playbook).toEqual('new baz type'); - wrapper - .find('CredentialChip') - .at(0) - .prop('onClick')(); + await act(async () => { + wrapper + .find('CredentialChip') + .at(0) + .prop('onClick')(); + }); expect(form.state('values').credentials).toEqual([ { id: 2, kind: 'ssh', name: 'Bar' }, ]); @@ -180,7 +182,9 @@ describe('', () => { }); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); expect(handleSubmit).not.toHaveBeenCalled(); - wrapper.find('button[aria-label="Save"]').simulate('click'); + await act(async () => { + wrapper.find('button[aria-label="Save"]').simulate('click'); + }); await sleep(1); expect(handleSubmit).toBeCalled(); });