diff --git a/awx/ui_next/src/api/models/CredentialTypes.js b/awx/ui_next/src/api/models/CredentialTypes.js index 65906cdcbd..d2d993091c 100644 --- a/awx/ui_next/src/api/models/CredentialTypes.js +++ b/awx/ui_next/src/api/models/CredentialTypes.js @@ -5,6 +5,28 @@ class CredentialTypes extends Base { super(http); this.baseUrl = '/api/v2/credential_types/'; } + + async loadAllTypes( + acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault'] + ) { + const pageSize = 200; + // The number of credential types a user can have is unlimited. In practice, it is unlikely for + // users to have more than a page at the maximum request size. + const { + data: { next, results }, + } = await this.read({ page_size: pageSize }); + let nextResults = []; + if (next) { + const { data } = await this.read({ + page_size: pageSize, + page: 2, + }); + nextResults = data.results; + } + return results + .concat(nextResults) + .filter(type => acceptableKinds.includes(type.kind)); + } } export default CredentialTypes; diff --git a/awx/ui_next/src/api/models/CredentialTypes.test.js b/awx/ui_next/src/api/models/CredentialTypes.test.js new file mode 100644 index 0000000000..5cb038912e --- /dev/null +++ b/awx/ui_next/src/api/models/CredentialTypes.test.js @@ -0,0 +1,65 @@ +import CredentialTypes from './CredentialTypes'; + +const typesData = [{ id: 1, kind: 'machine' }, { id: 2, kind: 'cloud' }]; + +describe('CredentialTypesAPI', () => { + test('should load all types', async () => { + const getPromise = () => + Promise.resolve({ + data: { + results: typesData, + }, + }); + const mockHttp = { get: jest.fn(getPromise) }; + const CredentialTypesAPI = new CredentialTypes(mockHttp); + + const types = await CredentialTypesAPI.loadAllTypes(); + + expect(mockHttp.get).toHaveBeenCalledTimes(1); + expect(mockHttp.get.mock.calls[0]).toEqual([ + `/api/v2/credential_types/`, + { params: { page_size: 200 } }, + ]); + expect(types).toEqual(typesData); + }); + + test('should load all types (2 pages)', async () => { + const getPromise = () => + Promise.resolve({ + data: { + results: typesData, + next: 2, + }, + }); + const mockHttp = { get: jest.fn(getPromise) }; + const CredentialTypesAPI = new CredentialTypes(mockHttp); + + const types = await CredentialTypesAPI.loadAllTypes(); + + expect(mockHttp.get).toHaveBeenCalledTimes(2); + expect(mockHttp.get.mock.calls[0]).toEqual([ + `/api/v2/credential_types/`, + { params: { page_size: 200 } }, + ]); + expect(mockHttp.get.mock.calls[1]).toEqual([ + `/api/v2/credential_types/`, + { params: { page_size: 200, page: 2 } }, + ]); + expect(types).toHaveLength(4); + }); + + test('should filter by acceptable kinds', async () => { + const getPromise = () => + Promise.resolve({ + data: { + results: typesData, + }, + }); + const mockHttp = { get: jest.fn(getPromise) }; + const CredentialTypesAPI = new CredentialTypes(mockHttp); + + const types = await CredentialTypesAPI.loadAllTypes(['machine']); + + expect(types).toEqual([typesData[0]]); + }); +}); diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index 1d232f0a87..7d48a7bba9 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -88,8 +88,8 @@ class LaunchButton extends React.Component { const { history, resource } = this.props; const jobPromise = resource.type === 'workflow_job_template' - ? WorkflowJobTemplatesAPI.launch(resource.id, params) - : JobTemplatesAPI.launch(resource.id, params); + ? WorkflowJobTemplatesAPI.launch(resource.id, params || {}) + : JobTemplatesAPI.launch(resource.id, params || {}); const { data: job } = await jobPromise; history.push( diff --git a/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx index 10872e311c..a389db0cff 100644 --- a/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx @@ -1,7 +1,171 @@ -import React from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { ToolbarItem } from '@patternfly/react-core'; +import { CredentialsAPI, CredentialTypesAPI } from '@api'; +import AnsibleSelect from '@components/AnsibleSelect'; +import OptionsList from '@components/OptionsList'; +import ContentLoading from '@components/ContentLoading'; +import CredentialChip from '@components/CredentialChip'; +import ContentError from '@components/ContentError'; +import { getQSConfig, parseQueryString } from '@util/qs'; +import useRequest from '@util/useRequest'; -function CredentialsStep() { - return
; +const QS_CONFIG = getQSConfig('inventory', { + page: 1, + page_size: 5, + order_by: 'name', +}); + +function CredentialsStep({ i18n }) { + const [field, , helpers] = useField('credentials'); + const [selectedType, setSelectedType] = useState(null); + const history = useHistory(); + + const { + result: types, + error: typesError, + isLoading: isTypesLoading, + request: fetchTypes, + } = useRequest( + useCallback(async () => { + const loadedTypes = await CredentialTypesAPI.loadAllTypes(); + if (loadedTypes.length) { + const match = + loadedTypes.find(type => type.kind === 'ssh') || loadedTypes[0]; + setSelectedType(match); + } + return loadedTypes; + }, []), + [] + ); + + useEffect(() => { + fetchTypes(); + }, [fetchTypes]); + + const { + result: { credentials, count }, + error: credentialsError, + isLoading: isCredentialsLoading, + request: fetchCredentials, + } = useRequest( + useCallback(async () => { + if (!selectedType) { + return { credentials: [], count: 0 }; + } + const params = parseQueryString(QS_CONFIG, history.location.search); + const { data } = await CredentialsAPI.read({ + ...params, + credential_type: selectedType.id, + }); + return { + credentials: data.results, + count: data.count, + }; + }, [selectedType, history.location.search]), + { credentials: [], count: 0 } + ); + + useEffect(() => { + fetchCredentials(); + }, [fetchCredentials]); + + if (isTypesLoading) { + return ; + } + + if (typesError || credentialsError) { + return ; + } + + const isVault = selectedType?.kind === 'vault'; + + const renderChip = ({ item, removeItem, canDelete }) => ( + removeItem(item)} + isReadOnly={!canDelete} + credential={item} + /> + ); + + return ( + <> + {types && types.length > 0 && ( + +
+ {i18n._(t`Selected Category`)} +
+ ({ + key: type.id, + value: type.id, + label: type.name, + isDisabled: false, + }))} + value={selectedType && selectedType.id} + onChange={(e, id) => { + setSelectedType(types.find(o => o.id === parseInt(id, 10))); + }} + /> +
+ )} + {!isCredentialsLoading && ( + { + const hasSameVaultID = val => + val?.inputs?.vault_id !== undefined && + val?.inputs?.vault_id === item?.inputs?.vault_id; + const hasSameKind = val => val.kind === item.kind; + const newItems = field.value.filter(i => + isVault ? !hasSameVaultID(i) : !hasSameKind(i) + ); + newItems.push(item); + helpers.setValue(newItems); + }} + deselectItem={item => { + helpers.setValue(field.value.filter(i => i.id !== item.id)); + }} + renderItemChip={renderChip} + /> + )} + + ); } -export default CredentialsStep; +export default withI18n()(CredentialsStep); diff --git a/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.test.jsx b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.test.jsx new file mode 100644 index 0000000000..5038482125 --- /dev/null +++ b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.test.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Formik } from 'formik'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import CredentialsStep from './CredentialsStep'; +import { CredentialsAPI, CredentialTypesAPI } from '@api'; + +jest.mock('@api/models/CredentialTypes'); +jest.mock('@api/models/Credentials'); + +const types = [ + { id: 1, kind: 'ssh', name: 'SSH' }, + { id: 2, kind: 'cloud', name: 'Ansible Tower' }, + { id: 3, kind: 'vault', name: 'Vault' }, +]; + +const credentials = [ + { id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' }, + { id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' }, + { id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' }, + { id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' }, + { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, +]; + +describe('CredentialsStep', () => { + beforeEach(() => { + CredentialTypesAPI.loadAllTypes.mockResolvedValue(types); + CredentialsAPI.read.mockResolvedValue({ + data: { + results: credentials, + count: 5, + }, + }); + }); + + test('should load credentials', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + + expect(CredentialsAPI.read).toHaveBeenCalled(); + expect(wrapper.find('OptionsList').prop('options')).toEqual(credentials); + }); + + test('should load credentials for selected type', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + + expect(CredentialsAPI.read).toHaveBeenCalledWith({ + credential_type: 1, + order_by: 'name', + page: 1, + page_size: 5, + }); + + await act(async () => { + wrapper.find('AnsibleSelect').invoke('onChange')({}, 2); + }); + expect(CredentialsAPI.read).toHaveBeenCalledWith({ + credential_type: 2, + order_by: 'name', + page: 1, + page_size: 5, + }); + }); +}); diff --git a/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx index 696dc445bc..e34dc40664 100644 --- a/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx @@ -8,6 +8,7 @@ import { getQSConfig, parseQueryString } from '@util/qs'; import useRequest from '@util/useRequest'; import OptionsList from '@components/OptionsList'; import ContentLoading from '@components/ContentLoading'; +import ContentError from '@components/ContentError'; const QS_CONFIG = getQSConfig('inventory', { page: 1, @@ -16,12 +17,12 @@ const QS_CONFIG = getQSConfig('inventory', { }); function InventoryStep({ i18n }) { - const history = useHistory(); const [field, , helpers] = useField('inventory'); + const history = useHistory(); const { isLoading, - // error, + error, result: { inventories, count }, request: fetchInventories, } = useRequest( @@ -46,6 +47,9 @@ function InventoryStep({ i18n }) { if (isLoading) { return ; } + if (error) { + return ; + } return ( , }); } - // TODO: match old UI Logic: - // if (vm.promptDataClone.launchConf.ask_credential_on_launch || - // (_.has(vm, 'promptDataClone.prompts.credentials.passwords.vault') && - // vm.promptDataClone.prompts.credentials.passwords.vault.length > 0) || - // _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_key_unlock') || - // _.has(vm, 'promptDataClone.prompts.credentials.passwords.become_password') || - // _.has(vm, 'promptDataClone.prompts.credentials.passwords.ssh_password') - // ) { if (config.ask_credential_on_launch) { initialValues.credentials = resource?.summary_fields?.credentials || []; steps.push({ - name: i18n._(t`Credential`), + name: i18n._(t`Credentials`), component: , }); } + + // TODO: Add Credential Passwords step + + if (config.ask_job_type_on_launch) { + initialValues.job_type = resource.job_type || ''; + } + if (config.ask_limit_on_launch) { + initialValues.limit = resource.limit || ''; + } + if (config.ask_verbosity_on_launch) { + initialValues.verbosity = resource.verbosity || 0; + } + if (config.ask_tags_on_launch) { + initialValues.job_tags = resource.job_tags || ''; + } + if (config.ask_skip_tags_on_launch) { + initialValues.skip_tags = resource.skip_tags || ''; + } + if (config.ask_variables_on_launch) { + initialValues.extra_vars = resource.extra_vars || '---'; + } + if (config.ask_scm_branch_on_launch) { + initialValues.scm_branch = resource.scm_branch || ''; + } + if (config.ask_diff_mode_on_launch) { + initialValues.diff_mode = resource.diff_mode || false; + } if ( - config.ask_scm_branch_on_launch || - (config.ask_variables_on_launch && !config.ignore_ask_variables) || - config.ask_tags_on_launch || - config.ask_diff_mode_on_launch || - config.ask_skip_tags_on_launch || config.ask_job_type_on_launch || config.ask_limit_on_launch || - config.ask_verbosity_on_launch + config.ask_verbosity_on_launch || + config.ask_tags_on_launch || + config.ask_skip_tags_on_launch || + config.ask_variables_on_launch || + config.ask_scm_branch_on_launch || + config.ask_diff_mode_on_launch ) { steps.push({ name: i18n._(t`Other Prompts`), @@ -63,9 +82,18 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) { const submit = values => { const postValues = {}; - if (values.inventory) { - postValues.inventory_id = values.inventory.id; - } + const setValue = (key, value) => { + if (typeof value !== 'undefined' && value !== null) { + postValues[key] = value; + } + }; + setValue('inventory_id', values.inventory?.id); + setValue('credentials', values.credentials?.map(c => c.id)); + setValue('job_type', values.job_type); + setValue('limit', values.limit); + setValue('job_tags', values.job_tags); + setValue('skip_tags', values.skip_tags); + setValue('extra_vars', values.extra_vars); onLaunch(postValues); }; diff --git a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx index edbb0cd7dc..3a490db6bc 100644 --- a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.test.jsx @@ -3,6 +3,8 @@ import { act, isElementOfType } from 'react-dom/test-utils'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; import LaunchPrompt from './LaunchPrompt'; import InventoryStep from './InventoryStep'; +import CredentialsStep from './CredentialsStep'; +import OtherPromptsStep from './OtherPromptsStep'; import PreviewStep from './PreviewStep'; import { InventoriesAPI } from '@api'; @@ -69,7 +71,7 @@ describe('LaunchPrompt', () => { expect(steps).toHaveLength(5); expect(steps[0].name).toEqual('Inventory'); - expect(steps[1].name).toEqual('Credential'); + expect(steps[1].name).toEqual('Credentials'); expect(steps[2].name).toEqual('Other Prompts'); expect(steps[3].name).toEqual('Survey'); expect(steps[4].name).toEqual('Preview'); @@ -97,4 +99,50 @@ describe('LaunchPrompt', () => { expect(isElementOfType(steps[0].component, InventoryStep)).toEqual(true); expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true); }); + + test('should add credentials step', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + const steps = wrapper.find('Wizard').prop('steps'); + + expect(steps).toHaveLength(2); + expect(steps[0].name).toEqual('Credentials'); + expect(isElementOfType(steps[0].component, CredentialsStep)).toEqual(true); + expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true); + }); + + test('should add other prompts step', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + const steps = wrapper.find('Wizard').prop('steps'); + + expect(steps).toHaveLength(2); + expect(steps[0].name).toEqual('Other Prompts'); + expect(isElementOfType(steps[0].component, OtherPromptsStep)).toEqual(true); + expect(isElementOfType(steps[1].component, PreviewStep)).toEqual(true); + }); }); diff --git a/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx index c0b8e6ec14..0989368652 100644 --- a/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx @@ -1,7 +1,180 @@ import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { Form, FormGroup, Switch } from '@patternfly/react-core'; +import FormField, { FieldTooltip } from '@components/FormField'; +import { TagMultiSelect } from '@components/MultiSelect'; +import AnsibleSelect from '@components/AnsibleSelect'; +import { VariablesField } from '@components/CodeMirrorInput'; +import styled from 'styled-components'; -function InventoryStep() { - return
; +const FieldHeader = styled.div` + display: flex; + justify-content: space-between; + padding-bottom: var(--pf-c-form__label--PaddingBottom); + + label { + --pf-c-form__label--PaddingBottom: 0px; + } +`; + +function OtherPromptsStep({ config, i18n }) { + return ( +
+ {config.ask_job_type_on_launch && } + {config.ask_limit_on_launch && ( + + )} + {config.ask_verbosity_on_launch && } + {config.ask_diff_mode_on_launch && } + {config.ask_tags_on_launch && ( + + )} + {config.ask_skip_tags_on_launch && ( + + )} + {config.ask_variables_on_launch && ( + + )} + + ); } -export default InventoryStep; +function JobTypeField({ i18n }) { + const [field, meta, helpers] = useField('job_type'); + const options = [ + { + value: '', + key: '', + label: i18n._(t`Choose a job type`), + isDisabled: true, + }, + { value: 'run', key: 'run', label: i18n._(t`Run`), isDisabled: false }, + { + value: 'check', + key: 'check', + label: i18n._(t`Check`), + isDisabled: false, + }, + ]; + const isValid = !(meta.touched && meta.error); + return ( + + + helpers.setValue(value)} + /> + + ); +} + +function VerbosityField({ i18n }) { + const [field, meta, helpers] = useField('verbosity'); + const options = [ + { value: '0', key: '0', label: i18n._(t`0 (Normal)`) }, + { value: '1', key: '1', label: i18n._(t`1 (Verbose)`) }, + { value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) }, + { value: '3', key: '3', label: i18n._(t`3 (Debug)`) }, + { value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) }, + ]; + const isValid = !(meta.touched && meta.error); + + return ( + + + helpers.setValue(value)} + /> + + ); +} + +function ShowChangesToggle({ i18n }) { + const [field, , helpers] = useField('diff_mode'); + return ( + + + {' '} + + + + + ); +} + +function TagField({ id, name, label, tooltip }) { + const [field, , helpers] = useField(name); + return ( + + + + + ); +} + +export default withI18n()(OtherPromptsStep); diff --git a/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.test.jsx b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.test.jsx new file mode 100644 index 0000000000..58f0856131 --- /dev/null +++ b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.test.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Formik } from 'formik'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import OtherPromptsStep from './OtherPromptsStep'; + +describe('OtherPromptsStep', () => { + test('should render job type field', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + + expect(wrapper.find('JobTypeField')).toHaveLength(1); + expect( + wrapper.find('JobTypeField AnsibleSelect').prop('data') + ).toHaveLength(3); + expect(wrapper.find('JobTypeField AnsibleSelect').prop('value')).toEqual( + 'run' + ); + }); + + test('should render limit field', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + + expect(wrapper.find('FormField#prompt-limit')).toHaveLength(1); + expect(wrapper.find('FormField#prompt-limit input').prop('name')).toEqual( + 'limit' + ); + }); + + test('should render verbosity field', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + + expect(wrapper.find('VerbosityField')).toHaveLength(1); + expect( + wrapper.find('VerbosityField AnsibleSelect').prop('data') + ).toHaveLength(5); + }); + + test('should render show changes toggle', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + + expect(wrapper.find('ShowChangesToggle')).toHaveLength(1); + expect(wrapper.find('ShowChangesToggle Switch').prop('isChecked')).toEqual( + true + ); + }); +}); diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx index 58bdf359cb..c153b1d610 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.jsx @@ -17,27 +17,6 @@ const QS_CONFIG = getQSConfig('credentials', { order_by: 'name', }); -async function loadCredentialTypes() { - const pageSize = 200; - const acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault']; - // The number of credential types a user can have is unlimited. In practice, it is unlikely for - // users to have more than a page at the maximum request size. - const { - data: { next, results }, - } = await CredentialTypesAPI.read({ page_size: pageSize }); - let nextResults = []; - if (next) { - const { data } = await CredentialTypesAPI.read({ - page_size: pageSize, - page: 2, - }); - nextResults = data.results; - } - return results - .concat(nextResults) - .filter(type => acceptableKinds.includes(type.kind)); -} - async function loadCredentials(params, selectedCredentialTypeId) { params.credential_type = selectedCredentialTypeId || 1; const { data } = await CredentialsAPI.read(params); @@ -54,7 +33,7 @@ function MultiCredentialsLookup(props) { useEffect(() => { (async () => { try { - const types = await loadCredentialTypes(); + const types = await CredentialTypesAPI.loadAllTypes(); setCredentialTypes(types); const match = types.find(type => type.kind === 'ssh') || types[0]; setSelectedType(match); diff --git a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx index fa8eb5a15f..08fcf87b18 100644 --- a/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx +++ b/awx/ui_next/src/components/Lookup/MultiCredentialsLookup.test.jsx @@ -18,21 +18,16 @@ describe('', () => { ]; beforeEach(() => { - CredentialTypesAPI.read.mockResolvedValueOnce({ - data: { - results: [ - { - id: 400, - kind: 'ssh', - namespace: 'biz', - name: 'Amazon Web Services', - }, - { id: 500, kind: 'vault', namespace: 'buzz', name: 'Vault' }, - { id: 600, kind: 'machine', namespace: 'fuzz', name: 'Machine' }, - ], - count: 2, + CredentialTypesAPI.loadAllTypes.mockResolvedValueOnce([ + { + id: 400, + kind: 'ssh', + namespace: 'biz', + name: 'Amazon Web Services', }, - }); + { id: 500, kind: 'vault', namespace: 'buzz', name: 'Vault' }, + { id: 600, kind: 'machine', namespace: 'fuzz', name: 'Machine' }, + ]); CredentialsAPI.read.mockResolvedValueOnce({ data: { results: [ @@ -52,7 +47,7 @@ describe('', () => { wrapper.unmount(); }); - test('MultiCredentialsLookup renders properly', async () => { + test('should load credential types', async () => { const onChange = jest.fn(); await act(async () => { wrapper = mountWithContexts( @@ -64,8 +59,9 @@ describe('', () => { /> ); }); + wrapper.update(); expect(wrapper.find('MultiCredentialsLookup')).toHaveLength(1); - expect(CredentialTypesAPI.read).toHaveBeenCalled(); + expect(CredentialTypesAPI.loadAllTypes).toHaveBeenCalled(); }); test('onChange is called when you click to remove a credential from input', async () => { @@ -118,12 +114,12 @@ describe('', () => { count: 1, }, }); - expect(CredentialsAPI.read).toHaveBeenCalledTimes(2); + expect(CredentialsAPI.read).toHaveBeenCalledTimes(1); await act(async () => { select.invoke('onChange')({}, 500); }); wrapper.update(); - expect(CredentialsAPI.read).toHaveBeenCalledTimes(3); + expect(CredentialsAPI.read).toHaveBeenCalledTimes(2); expect(wrapper.find('OptionsList').prop('options')).toEqual([ { id: 1, kind: 'cloud', name: 'New Cred', url: 'www.google.com' }, ]);