diff --git a/awx/ui_next/src/components/Lookup/Lookup.jsx b/awx/ui_next/src/components/Lookup/Lookup.jsx index afb67b54c4..b431ce4ac2 100644 --- a/awx/ui_next/src/components/Lookup/Lookup.jsx +++ b/awx/ui_next/src/components/Lookup/Lookup.jsx @@ -30,6 +30,7 @@ const SearchButton = styled(Button)` var(--pf-global--BorderColor--200); } `; +SearchButton.displayName = 'SearchButton'; const InputGroup = styled(PFInputGroup)` ${props => @@ -120,7 +121,7 @@ function Lookup(props) { - + {(multiple ? value : [value]).map(item => renderItemChip({ item, diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 02764f3de3..2d1eff8965 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -32,7 +32,7 @@ async function loadCredentials(params, selectedCredentialTypeId) { } function MultiCredentialsLookup(props) { - const { history, tooltip, value, onChange, onError, i18n } = props; + const { tooltip, value, onChange, onError, history, i18n } = props; const [credentialTypes, setCredentialTypes] = useState([]); const [selectedType, setSelectedType] = useState(null); const [credentials, setCredentials] = useState([]); @@ -50,7 +50,7 @@ function MultiCredentialsLookup(props) { onError(err); } })(); - }, []); + }, [onError]); useEffect(() => { (async () => { @@ -69,7 +69,7 @@ function MultiCredentialsLookup(props) { onError(err); } })(); - }, [selectedType]); + }, [selectedType, history.location.search, onError]); const isMultiple = selectedType && selectedType.name === 'Vault'; const renderChip = ({ item, removeItem, canDelete }) => ( diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx index cc00396525..baaea96776 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx @@ -1,6 +1,7 @@ import React from 'react'; - -import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { sleep } from '@testUtils/testUtils'; import MultiCredentialsLookup from './MultiCredentialsLookup'; import { CredentialsAPI, CredentialTypesAPI } from '@api'; @@ -8,9 +9,6 @@ jest.mock('@api'); describe('', () => { let wrapper; - let lookup; - let credLookup; - let onChange; const credentials = [ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, @@ -18,8 +16,9 @@ describe('', () => { { name: 'Gatsby', id: 21, kind: 'vault' }, { name: 'Gatsby', id: 8, kind: 'Machine' }, ]; + beforeEach(() => { - CredentialTypesAPI.read.mockResolvedValue({ + CredentialTypesAPI.read.mockResolvedValueOnce({ data: { results: [ { @@ -46,17 +45,6 @@ describe('', () => { count: 3, }, }); - onChange = jest.fn(); - wrapper = mountWithContexts( - {}} - credentials={credentials} - onChange={onChange} - tooltip="This is credentials look up" - /> - ); - lookup = wrapper.find('Lookup'); - credLookup = wrapper.find('MultiCredentialsLookup'); }); afterEach(() => { @@ -64,16 +52,40 @@ describe('', () => { wrapper.unmount(); }); - test('MultiCredentialsLookup renders properly', () => { + test('MultiCredentialsLookup renders properly', async () => { + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); expect(wrapper.find('MultiCredentialsLookup')).toHaveLength(1); expect(CredentialTypesAPI.read).toHaveBeenCalled(); }); test('onChange is called when you click to remove a credential from input', async () => { - const chip = wrapper.find('PFChip').find({ isOverflowChip: false }); - const button = chip.at(1).find('ChipButton'); + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + const chip = wrapper.find('CredentialChip'); expect(chip).toHaveLength(4); - button.prop('onClick')(); + const button = chip.at(1).find('ChipButton'); + await act(async () => { + button.invoke('onClick')(); + }); expect(onChange).toBeCalledWith([ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, { id: 21, kind: 'vault', name: 'Gatsby' }, @@ -81,33 +93,122 @@ describe('', () => { ]); }); - test('can change credential types', () => { - lookup.prop('selectCategory')({}, 'Vault'); - expect(credLookup.state('selectedCredentialType')).toEqual({ - id: 500, - key: 500, - kind: 'vault', - type: 'buzz', - value: 'Vault', - label: 'Vault', - isDisabled: false, + test('should change credential types', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + onError={() => {}} + /> + ); }); - expect(CredentialsAPI.read).toHaveBeenCalled(); + const searchButton = await waitForElement(wrapper, 'SearchButton'); + await act(async () => { + searchButton.invoke('onClick')(); + }); + const select = await waitForElement(wrapper, 'AnsibleSelect'); + CredentialsAPI.read.mockResolvedValueOnce({ + data: { + results: [ + { id: 1, kind: 'cloud', name: 'New Cred', url: 'www.google.com' }, + ], + count: 1, + }, + }); + expect(CredentialsAPI.read).toHaveBeenCalledTimes(2); + await act(async () => { + select.invoke('onChange')({}, 500); + }); + wrapper.update(); + expect(CredentialsAPI.read).toHaveBeenCalledTimes(3); + expect(wrapper.find('OptionsList').prop('options')).toEqual([ + { id: 1, kind: 'cloud', name: 'New Cred', url: 'www.google.com' }, + ]); }); - test('Toggle credentials only adds 1 credential per credential type except vault(see below)', () => { - lookup.prop('onToggleItem')({ name: 'Party', id: 9, kind: 'Machine' }); + + test('should only add 1 credential per credential type except vault(see below)', async () => { + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + const searchButton = await waitForElement(wrapper, 'SearchButton'); + await act(async () => { + searchButton.invoke('onClick')(); + }); + wrapper.update(); + const optionsList = wrapper.find('OptionsList'); + expect(optionsList.prop('multiple')).toEqual(false); + act(() => { + optionsList.invoke('selectItem')({ + id: 5, + kind: 'Machine', + name: 'Cred 5', + url: 'www.google.com', + }); + }); + wrapper.update(); + act(() => { + wrapper.find('Button[variant="primary"]').invoke('onClick')(); + }); expect(onChange).toBeCalledWith([ { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, { id: 21, kind: 'vault', name: 'Gatsby' }, - { id: 9, kind: 'Machine', name: 'Party' }, + { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, ]); }); - test('Toggle credentials only adds 1 credential per credential type', () => { - lookup.prop('onToggleItem')({ name: 'Party', id: 22, kind: 'vault' }); + + test('should allow multiple vault credentials', async () => { + const onChange = jest.fn(); + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + const searchButton = await waitForElement(wrapper, 'SearchButton'); + await act(async () => { + searchButton.invoke('onClick')(); + }); + wrapper.update(); + const typeSelect = wrapper.find('AnsibleSelect'); + act(() => { + typeSelect.invoke('onChange')({}, 500); + }); + wrapper.update(); + const optionsList = wrapper.find('OptionsList'); + expect(optionsList.prop('multiple')).toEqual(true); + act(() => { + optionsList.invoke('selectItem')({ + id: 5, + kind: 'Machine', + name: 'Cred 5', + url: 'www.google.com', + }); + }); + wrapper.update(); + act(() => { + wrapper.find('Button[variant="primary"]').invoke('onClick')(); + }); expect(onChange).toBeCalledWith([ - ...credentials, - { name: 'Party', id: 22, kind: 'vault' }, + { id: 1, kind: 'cloud', name: 'Foo', url: 'www.google.com' }, + { id: 2, kind: 'ssh', name: 'Alex', url: 'www.google.com' }, + { id: 21, kind: 'vault', name: 'Gatsby' }, + { id: 8, kind: 'Machine', name: 'Gatsby' }, + { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, ]); }); });