diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx new file mode 100644 index 0000000000..ad2c254f06 --- /dev/null +++ b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.jsx @@ -0,0 +1,155 @@ +import React, { useState, useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import useRequest, { useDismissableError } from '../../util/useRequest'; +import SelectableCard from '../SelectableCard'; +import AlertModal from '../AlertModal'; +import ErrorDetail from '../ErrorDetail'; +import Wizard from '../Wizard/Wizard'; +import useSelected from '../../util/useSelected'; +import SelectResourceStep from '../AddRole/SelectResourceStep'; +import SelectRoleStep from '../AddRole/SelectRoleStep'; +import getResourceTypes from './resources.data'; + +const Grid = styled.div` + display: grid; + grid-gap: 20px; + grid-template-columns: 33% 33% 33%; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +`; + +function UserAndTeamAccessAdd({ + i18n, + isOpen, + title, + onSave, + apiModel, + onClose, +}) { + const [selectedResourceType, setSelectedResourceType] = useState(); + const [stepIdReached, setStepIdReached] = useState(1); + const { id: userId } = useParams(); + const { + selected: resourcesSelected, + handleSelect: handleResourceSelect, + } = useSelected([]); + + const { + selected: rolesSelected, + handleSelect: handleRoleSelect, + } = useSelected([]); + + const { request: handleWizardSave, error: saveError } = useRequest( + useCallback(async () => { + const roleRequests = []; + const resourceRolesTypes = resourcesSelected.flatMap(resource => + Object.values(resource.summary_fields.object_roles) + ); + + rolesSelected.map(role => + resourceRolesTypes.forEach(rolename => { + if (rolename.name === role.name) { + roleRequests.push(apiModel.associateRole(userId, rolename.id)); + } + }) + ); + + await Promise.all(roleRequests); + onSave(); + }, [onSave, rolesSelected, apiModel, userId, resourcesSelected]), + {} + ); + + const { error, dismissError } = useDismissableError(saveError); + + const steps = [ + { + id: 1, + name: i18n._(t`Add resource type`), + component: ( + + {getResourceTypes(i18n).map(resource => ( + setSelectedResourceType(resource)} + /> + ))} + + ), + }, + { + id: 2, + name: i18n._(t`Select items from list`), + component: selectedResourceType && ( + + ), + enableNext: resourcesSelected.length > 0, + canJumpTo: stepIdReached >= 2, + }, + { + id: 3, + name: i18n._(t`Select roles to apply`), + component: resourcesSelected?.length > 0 && ( + + ), + nextButtonText: i18n._(t`Save`), + canJumpTo: stepIdReached >= 3, + }, + ]; + + if (error) { + return ( + + {i18n._(t`Failed to associate role`)} + + + ); + } + + return ( + + setStepIdReached(stepIdReached < id ? id : stepIdReached) + } + onSave={handleWizardSave} + /> + ); +} + +export default withI18n()(UserAndTeamAccessAdd); diff --git a/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx new file mode 100644 index 0000000000..0a703a7024 --- /dev/null +++ b/awx/ui_next/src/components/UserAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -0,0 +1,225 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { UsersAPI, JobTemplatesAPI } from '../../api'; +import { + mountWithContexts, + waitForElement, +} from '../../../testUtils/enzymeHelpers'; +import UserAndTeamAccessAdd from './UserAndTeamAccessAdd'; + +jest.mock('../../api/models/Teams'); +jest.mock('../../api/models/Users'); +jest.mock('../../api/models/JobTemplates'); + +describe('', () => { + const resources = { + data: { + results: [ + { + id: 1, + name: 'Job Template Foo Bar', + url: '/api/v2/job_template/1/', + summary_fields: { + object_roles: { + admin_role: { + description: 'Can manage all aspects of the job template', + name: 'Admin', + id: 164, + }, + execute_role: { + description: 'May run the job template', + name: 'Execute', + id: 165, + }, + read_role: { + description: 'May view settings for the job template', + name: 'Read', + id: 166, + }, + }, + }, + }, + ], + count: 1, + }, + }; + let wrapper; + beforeEach(async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + onClose={() => {}} + title="Add user permissions" + /> + ); + }); + }); + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + test('should mount properly', async () => { + expect(wrapper.find('PFWizard').length).toBe(1); + }); + test('should disable steps', async () => { + expect( + wrapper + .find('WizardNavItem[text="Select ttems from list"]') + .prop('isDisabled') + ).toBe(true); + expect( + wrapper + .find('WizardNavItem[text="Select roles to apply"]') + .prop('isDisabled') + ).toBe(true); + await act(async () => + wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ + fetchItems: JobTemplatesAPI.read, + label: 'Job template', + selectedResource: 'jobTemplate', + searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + sortColumns: [{ name: 'Name', key: 'name' }], + }) + ); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + wrapper.update(); + expect( + wrapper.find('WizardNavItem[text="Add resource type"]').prop('isDisabled') + ).toBe(false); + expect( + wrapper + .find('WizardNavItem[text="Select ttems from list"]') + .prop('isDisabled') + ).toBe(false); + expect( + wrapper + .find('WizardNavItem[text="Select roles to apply"]') + .prop('isDisabled') + ).toBe(true); + }); + + test('should call api to associate role', async () => { + JobTemplatesAPI.read.mockResolvedValue(resources); + UsersAPI.associateRole.mockResolvedValue({}); + + await act(async () => + wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ + fetchItems: JobTemplatesAPI.read, + label: 'Job template', + selectedResource: 'jobTemplate', + searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + sortColumns: [{ name: 'Name', key: 'name' }], + }) + ); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0); + expect(JobTemplatesAPI.read).toHaveBeenCalled(); + await act(async () => + wrapper + .find('CheckboxListItem') + .first() + .find('input[type="checkbox"]') + .simulate('change', { target: { checked: true } }) + ); + + wrapper.update(); + + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + + wrapper.update(); + + expect(wrapper.find('RolesStep').length).toBe(1); + + await act(async () => + wrapper + .find('CheckboxCard') + .first() + .prop('onSelect')() + ); + + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + + await expect(UsersAPI.associateRole).toHaveBeenCalled(); + }); + + test('should throw error', async () => { + JobTemplatesAPI.read.mockResolvedValue(resources); + UsersAPI.associateRole.mockRejectedValue( + new Error({ + response: { + config: { + method: 'post', + url: '/api/v2/users/a/roles', + }, + data: 'An error occurred', + status: 403, + }, + }) + ); + + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 'a', + }), + })); + + await act(async () => + wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ + fetchItems: JobTemplatesAPI.read, + label: 'Job template', + selectedResource: 'jobTemplate', + searchColumns: [{ name: 'Name', key: 'name', isDefault: true }], + sortColumns: [{ name: 'Name', key: 'name' }], + }) + ); + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + await waitForElement(wrapper, 'SelectResourceStep', el => el.length > 0); + expect(JobTemplatesAPI.read).toHaveBeenCalled(); + await act(async () => + wrapper + .find('CheckboxListItem') + .first() + .find('input[type="checkbox"]') + .simulate('change', { target: { checked: true } }) + ); + + wrapper.update(); + + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + + wrapper.update(); + + expect(wrapper.find('RolesStep').length).toBe(1); + + await act(async () => + wrapper + .find('CheckboxCard') + .first() + .prop('onSelect')() + ); + + await act(async () => + wrapper.find('Button[type="submit"]').prop('onClick')() + ); + + await expect(UsersAPI.associateRole).toHaveBeenCalled(); + wrapper.update(); + expect(wrapper.find('AlertModal').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/components/UserAccessAdd/index.js b/awx/ui_next/src/components/UserAccessAdd/index.js new file mode 100644 index 0000000000..c445f018ba --- /dev/null +++ b/awx/ui_next/src/components/UserAccessAdd/index.js @@ -0,0 +1 @@ +export { default } from './UserAndTeamAccessAdd'; diff --git a/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx b/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx new file mode 100644 index 0000000000..0000036c59 --- /dev/null +++ b/awx/ui_next/src/components/UserAccessAdd/resources.data.jsx @@ -0,0 +1,208 @@ +import { t } from '@lingui/macro'; +import { + JobTemplatesAPI, + WorkflowJobTemplatesAPI, + CredentialsAPI, + InventoriesAPI, + ProjectsAPI, + OrganizationsAPI, +} from '../../api'; + +export default function getResourceTypes(i18n) { + return [ + { + selectedResource: 'jobTemplate', + label: i18n._(t`Job templates`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Playbook name`), + key: 'playbook', + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => JobTemplatesAPI.read(), + }, + { + selectedResource: 'workflowJobTemplate', + label: i18n._(t`Workflow job templates`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Playbook name`), + key: 'playbook', + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => WorkflowJobTemplatesAPI.read(), + }, + { + selectedResource: 'credential', + label: i18n._(t`Credentials`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Type`), + key: 'scm_type', + options: [ + [``, i18n._(t`Manual`)], + [`git`, i18n._(t`Git`)], + [`hg`, i18n._(t`Mercurial`)], + [`svn`, i18n._(t`Subversion`)], + [`insights`, i18n._(t`Red Hat Insights`)], + ], + }, + { + name: i18n._(t`Source Control URL`), + key: 'scm_url', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => CredentialsAPI.read(), + }, + { + selectedResource: 'inventory', + label: i18n._(t`Inventories`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => InventoriesAPI.read(), + }, + { + selectedResource: 'project', + label: i18n._(t`Projects`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Type`), + key: 'scm_type', + options: [ + [``, i18n._(t`Manual`)], + [`git`, i18n._(t`Git`)], + [`hg`, i18n._(t`Mercurial`)], + [`svn`, i18n._(t`Subversion`)], + [`insights`, i18n._(t`Red Hat Insights`)], + ], + }, + { + name: i18n._(t`Source Control URL`), + key: 'scm_url', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => ProjectsAPI.read(), + }, + { + selectedResource: 'organization', + label: i18n._(t`Organizations`), + searchColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + isDefault: true, + }, + { + name: i18n._(t`Created By (Username)`), + key: 'created_by__username', + }, + { + name: i18n._(t`Modified By (Username)`), + key: 'modified_by__username', + }, + ], + sortColumns: [ + { + name: i18n._(t`Name`), + key: 'name', + }, + ], + fetchItems: () => OrganizationsAPI.read(), + }, + ]; +} diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx index 79531fb561..6c6f2c2910 100644 --- a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx +++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.jsx @@ -1,19 +1,18 @@ -import React, { useCallback, useEffect } from 'react'; -import { useLocation, useRouteMatch, useParams } from 'react-router-dom'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Card } from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; import { TeamsAPI } from '../../../api'; import useRequest from '../../../util/useRequest'; import DataListToolbar from '../../../components/DataListToolbar'; -import PaginatedDataList, { - ToolbarAddButton, -} from '../../../components/PaginatedDataList'; +import PaginatedDataList from '../../../components/PaginatedDataList'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import TeamAccessListItem from './TeamAccessListItem'; +import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; const QS_CONFIG = getQSConfig('team', { page: 1, @@ -22,8 +21,8 @@ const QS_CONFIG = getQSConfig('team', { }); function TeamAccessList({ i18n }) { + const [isWizardOpen, setIsWizardOpen] = useState(false); const { search } = useLocation(); - const match = useRouteMatch(); const { id } = useParams(); const { @@ -57,6 +56,11 @@ function TeamAccessList({ i18n }) { fetchRoles(); }, [fetchRoles]); + const saveRoles = () => { + setIsWizardOpen(false); + fetchRoles(); + }; + const canAdd = options && Object.prototype.hasOwnProperty.call(options, 'POST'); @@ -77,7 +81,7 @@ function TeamAccessList({ i18n }) { }; return ( - + <> ] + ? [ + , + ] : []), ]} /> @@ -117,13 +131,17 @@ function TeamAccessList({ i18n }) { onSelect={() => {}} /> )} - emptyStateControls={ - canAdd ? ( - - ) : null - } /> - + {isWizardOpen && ( + setIsWizardOpen(false)} + title={i18n._(t`Add team permissions`)} + /> + )} + ); } export default withI18n()(TeamAccessList); diff --git a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx index b7b5e26025..88311c8643 100644 --- a/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx +++ b/awx/ui_next/src/screens/Team/TeamAccess/TeamAccessList.test.jsx @@ -141,4 +141,67 @@ describe('', () => { '/inventories/smart_inventory/77/details' ); }); + test('should not render add button', async () => { + TeamsAPI.readRoleOptions.mockResolvedValueOnce({ + data: {}, + }); + + TeamsAPI.readRoles.mockResolvedValue({ + data: { + results: [ + { + id: 2, + name: 'Admin', + type: 'role', + url: '/api/v2/roles/257/', + summary_fields: { + resource_name: 'template delete project', + resource_id: 15, + resource_type: 'job_template', + resource_type_display_name: 'Job Template', + user_capabilities: { unattach: true }, + }, + description: 'Can manage all aspects of the job template', + }, + ], + count: 1, + }, + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 18 } }, + }, + }, + }, + } + ); + }); + + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe( + 0 + ); + }); + test('should open and close wizard', async () => { + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + await act(async () => + wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('PFWizard').length).toBe(1); + await act(async () => + wrapper.find("Button[aria-label='Close']").prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('PFWizard').length).toBe(0); + }); }); diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx index db62a14bd6..b44a83f303 100644 --- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx +++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx @@ -1,15 +1,16 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; + import { getQSConfig, parseQueryString } from '../../../util/qs'; import { UsersAPI } from '../../../api'; import useRequest from '../../../util/useRequest'; -import PaginatedDataList, { - ToolbarAddButton, -} from '../../../components/PaginatedDataList'; +import PaginatedDataList from '../../../components/PaginatedDataList'; import DatalistToolbar from '../../../components/DataListToolbar'; import UserAccessListItem from './UserAccessListItem'; +import UserAndTeamAccessAdd from '../../../components/UserAccessAdd/UserAndTeamAccessAdd'; const QS_CONFIG = getQSConfig('roles', { page: 1, @@ -22,6 +23,7 @@ const QS_CONFIG = getQSConfig('roles', { function UserAccessList({ i18n }) { const { id } = useParams(); const { search } = useLocation(); + const [isWizardOpen, setIsWizardOpen] = useState(false); const { isLoading, @@ -55,6 +57,11 @@ function UserAccessList({ i18n }) { const canAdd = options && Object.prototype.hasOwnProperty.call(options, 'POST'); + const saveRoles = () => { + setIsWizardOpen(false); + fetchRoles(); + }; + const detailUrl = role => { const { resource_id, resource_type } = role.summary_fields; @@ -72,48 +79,71 @@ function UserAccessList({ i18n }) { }; return ( - { - return ( - {}} - isSelected={false} + <> + { + return ( + {}} + isSelected={false} + /> + ); + }} + renderToolbar={props => ( + { + setIsWizardOpen(true); + }} + > + Add + , + ] + : []), + ]} /> - ); - }} - renderToolbar={props => ( - ] : []), - ]} + )} + /> + {isWizardOpen && ( + setIsWizardOpen(false)} + title={i18n._(t`Add user permissions`)} /> )} - /> + ); } export default withI18n()(UserAccessList); diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx index acb1400608..8a59012a30 100644 --- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx +++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.test.jsx @@ -29,6 +29,7 @@ describe('', () => { resource_type_display_name: 'Job Template', user_capabilities: { unattach: true }, }, + description: 'Can manage all aspects of the job template', }, { id: 3, @@ -42,6 +43,7 @@ describe('', () => { resource_type_display_name: 'Job Template', user_capabilities: { unattach: true }, }, + description: 'Can manage all aspects of the job template', }, { id: 4, @@ -55,10 +57,11 @@ describe('', () => { resource_type_display_name: 'Credential', user_capabilities: { unattach: true }, }, + description: 'May run the job template', }, { id: 5, - name: 'Update', + name: 'Read', type: 'role', url: '/api/v2/roles/259/', summary_fields: { @@ -68,6 +71,7 @@ describe('', () => { resource_type_display_name: 'Inventory', user_capabilities: { unattach: true }, }, + description: 'May view settings for the job template', }, { id: 6, @@ -75,15 +79,16 @@ describe('', () => { type: 'role', url: '/api/v2/roles/260/', summary_fields: { - resource_name: 'Smart Inventory Foo', + resource_name: 'Project Foo', resource_id: 77, - resource_type: 'smart_inventory', - resource_type_display_name: 'Inventory', + resource_type: 'project', + resource_type_display_name: 'Project', user_capabilities: { unattach: true }, }, + description: 'Can manage all aspects of the job template', }, ], - count: 4, + count: 5, }, }); @@ -138,7 +143,86 @@ describe('', () => { '/inventories/inventory/76/details' ); expect(wrapper.find('Link#userRole-6').prop('to')).toBe( - '/inventories/smart_inventory/77/details' + '/projects/77/details' ); }); + test('should not render add button', async () => { + UsersAPI.readRoleOptions.mockResolvedValueOnce({ + data: {}, + }); + + UsersAPI.readRoles.mockResolvedValue({ + data: { + results: [ + { + id: 2, + name: 'Admin', + type: 'role', + url: '/api/v2/roles/257/', + summary_fields: { + resource_name: 'template delete project', + resource_id: 15, + resource_type: 'job_template', + resource_type_display_name: 'Job Template', + user_capabilities: { unattach: true }, + object_roles: { + admin_role: { + description: 'Can manage all aspects of the job template', + name: 'Admin', + id: 164, + }, + execute_role: { + description: 'May run the job template', + name: 'Execute', + id: 165, + }, + read_role: { + description: 'May view settings for the job template', + name: 'Read', + id: 166, + }, + }, + }, + }, + ], + count: 1, + }, + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 18 } }, + }, + }, + }, + } + ); + }); + + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + expect(wrapper.find('Button[aria-label="Add resource roles"]').length).toBe( + 0 + ); + }); + test('should open and close wizard', async () => { + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + await act(async () => + wrapper.find('Button[aria-label="Add resource roles"]').prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('PFWizard').length).toBe(1); + await act(async () => + wrapper.find("Button[aria-label='Close']").prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('PFWizard').length).toBe(0); + }); });