Merge pull request #13598 from marshmalien/constructed-inventory-sync-button

Add constructed inventory detail sync button
This commit is contained in:
Marliana Lara
2023-02-22 10:20:45 -05:00
committed by GitHub
9 changed files with 509 additions and 102 deletions

View File

@@ -13,7 +13,7 @@ class Inventories extends InstanceGroupsMixin(Base) {
this.readGroups = this.readGroups.bind(this); this.readGroups = this.readGroups.bind(this);
this.readGroupsOptions = this.readGroupsOptions.bind(this); this.readGroupsOptions = this.readGroupsOptions.bind(this);
this.promoteGroup = this.promoteGroup.bind(this); this.promoteGroup = this.promoteGroup.bind(this);
this.readSourceInventories = this.readSourceInventories.bind(this); this.readInputInventories = this.readInputInventories.bind(this);
} }
readAccessList(id, params) { readAccessList(id, params) {
@@ -73,7 +73,7 @@ class Inventories extends InstanceGroupsMixin(Base) {
}); });
} }
readSourceInventories(inventoryId, params) { readInputInventories(inventoryId, params) {
return this.http.get(`${this.baseUrl}${inventoryId}/input_inventories/`, { return this.http.get(`${this.baseUrl}${inventoryId}/input_inventories/`, {
params, params,
}); });

View File

@@ -5,54 +5,103 @@ import { t } from '@lingui/macro';
import { import {
Button, Button,
Chip, Chip,
Label,
LabelGroup,
TextList, TextList,
TextListItem, TextListItem,
TextListItemVariants, TextListItemVariants,
TextListVariants, TextListVariants,
Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { InventoriesAPI, ConstructedInventoriesAPI } from 'api';
import { Inventory } from 'types';
import { formatDateString } from 'util/dates';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import AlertModal from 'components/AlertModal'; import AlertModal from 'components/AlertModal';
import { CardBody, CardActionsRow } from 'components/Card'; import { CardBody, CardActionsRow } from 'components/Card';
import { DetailList, Detail, UserDateDetail } from 'components/DetailList'; import ChipGroup from 'components/ChipGroup';
import { VariablesDetail } from 'components/CodeEditor'; import { VariablesDetail } from 'components/CodeEditor';
import DeleteButton from 'components/DeleteButton';
import ErrorDetail from 'components/ErrorDetail';
import ContentError from 'components/ContentError'; import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading'; import ContentLoading from 'components/ContentLoading';
import ChipGroup from 'components/ChipGroup'; import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
import Popover from 'components/Popover'; import DeleteButton from 'components/DeleteButton';
import { InventoriesAPI, ConstructedInventoriesAPI } from 'api'; import ErrorDetail from 'components/ErrorDetail';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import { Inventory } from 'types';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import InstanceGroupLabels from 'components/InstanceGroupLabels'; import InstanceGroupLabels from 'components/InstanceGroupLabels';
import JobCancelButton from 'components/JobCancelButton';
import Popover from 'components/Popover';
import StatusLabel from 'components/StatusLabel';
import ConstructedInventorySyncButton from './ConstructedInventorySyncButton';
import useWsInventorySourcesDetails from '../shared/useWsInventorySourcesDetails';
import getHelpText from '../shared/Inventory.helptext'; import getHelpText from '../shared/Inventory.helptext';
function JobStatusLabel({ job }) {
if (!job) {
return null;
}
return (
<Tooltip
position="top"
content={
<>
<div>{t`MOST RECENT SYNC`}</div>
<div>
{t`JOB ID:`} {job.id}
</div>
<div>
{t`STATUS:`} {job.status.toUpperCase()}
</div>
{job.finished && (
<div>
{t`FINISHED:`} {formatDateString(job.finished)}
</div>
)}
</>
}
key={job.id}
>
<Link to={`/jobs/inventory/${job.id}`}>
<StatusLabel status={job.status} />
</Link>
</Tooltip>
);
}
function ConstructedInventoryDetail({ inventory }) { function ConstructedInventoryDetail({ inventory }) {
const history = useHistory(); const history = useHistory();
const helpText = getHelpText(); const helpText = getHelpText();
const { const {
result: { instanceGroups, sourceInventories, actions }, result: { instanceGroups, inputInventories, inventorySource, actions },
request: fetchRelatedDetails, request: fetchRelatedDetails,
error: contentError, error: contentError,
isLoading, isLoading,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const [response, sourceInvResponse, options] = await Promise.all([ const [
instanceGroupsResponse,
inputInventoriesResponse,
inventorySourceResponse,
optionsResponse,
] = await Promise.all([
InventoriesAPI.readInstanceGroups(inventory.id), InventoriesAPI.readInstanceGroups(inventory.id),
InventoriesAPI.readSourceInventories(inventory.id), InventoriesAPI.readInputInventories(inventory.id),
ConstructedInventoriesAPI.readOptions(inventory.id), InventoriesAPI.readSources(inventory.id),
ConstructedInventoriesAPI.readOptions(),
]); ]);
return { return {
instanceGroups: response.data.results, instanceGroups: instanceGroupsResponse.data.results,
sourceInventories: sourceInvResponse.data.results, inputInventories: inputInventoriesResponse.data.results,
actions: options.data.actions.GET, inventorySource: inventorySourceResponse.data.results[0],
actions: optionsResponse.data.actions.GET,
}; };
}, [inventory.id]), }, [inventory.id]),
{ {
instanceGroups: [], instanceGroups: [],
sourceInventories: [], inputInventories: [],
inventorySource: {},
actions: {}, actions: {},
isLoading: true, isLoading: true,
} }
@@ -62,6 +111,16 @@ function ConstructedInventoryDetail({ inventory }) {
fetchRelatedDetails(); fetchRelatedDetails();
}, [fetchRelatedDetails]); }, [fetchRelatedDetails]);
const wsInventorySource = useWsInventorySourcesDetails(inventorySource);
const inventorySourceSyncJob =
wsInventorySource.summary_fields?.current_job ||
wsInventorySource.summary_fields?.last_job ||
null;
const wsInventory = {
...inventory,
...wsInventorySource?.summary_fields?.inventory,
};
const { request: deleteInventory, error: deleteError } = useRequest( const { request: deleteInventory, error: deleteError } = useRequest(
useCallback(async () => { useCallback(async () => {
await InventoriesAPI.destroy(inventory.id); await InventoriesAPI.destroy(inventory.id);
@@ -71,9 +130,6 @@ function ConstructedInventoryDetail({ inventory }) {
const { error, dismissError } = useDismissableError(deleteError); const { error, dismissError } = useDismissableError(deleteError);
const { organization, user_capabilities: userCapabilities } =
inventory.summary_fields;
const deleteDetailsRequests = const deleteDetailsRequests =
relatedResourceDeleteRequests.inventory(inventory); relatedResourceDeleteRequests.inventory(inventory);
@@ -93,6 +149,14 @@ function ConstructedInventoryDetail({ inventory }) {
value={inventory.name} value={inventory.name}
dataCy="constructed-inventory-name" dataCy="constructed-inventory-name"
/> />
<Detail
label={t`Last Job Status`}
value={
inventorySourceSyncJob && (
<JobStatusLabel job={inventorySourceSyncJob} />
)
}
/>
<Detail <Detail
label={t`Description`} label={t`Description`}
value={inventory.description} value={inventory.description}
@@ -113,26 +177,28 @@ function ConstructedInventoryDetail({ inventory }) {
label={t`Organization`} label={t`Organization`}
dataCy="constructed-inventory-organization" dataCy="constructed-inventory-organization"
value={ value={
<Link to={`/organizations/${organization.id}/details`}> <Link
{organization.name} to={`/organizations/${inventory.summary_fields?.organization.id}/details`}
>
{inventory.summary_fields?.organization.name}
</Link> </Link>
} }
/> />
<Detail <Detail
label={actions.total_groups.label} label={actions.total_groups.label}
value={inventory.total_groups} value={wsInventory.total_groups}
helpText={actions.total_groups.help_text} helpText={actions.total_groups.help_text}
dataCy="constructed-inventory-total-groups" dataCy="constructed-inventory-total-groups"
/> />
<Detail <Detail
label={actions.total_hosts.label} label={actions.total_hosts.label}
value={inventory.total_hosts} value={wsInventory.total_hosts}
helpText={actions.total_hosts.help_text} helpText={actions.total_hosts.help_text}
dataCy="constructed-inventory-total-hosts" dataCy="constructed-inventory-total-hosts"
/> />
<Detail <Detail
label={actions.total_inventory_sources.label} label={actions.total_inventory_sources.label}
value={inventory.total_inventory_sources} value={wsInventory.total_inventory_sources}
helpText={actions.total_inventory_sources.help_text} helpText={actions.total_inventory_sources.help_text}
dataCy="constructed-inventory-sources" dataCy="constructed-inventory-sources"
/> />
@@ -144,7 +210,7 @@ function ConstructedInventoryDetail({ inventory }) {
/> />
<Detail <Detail
label={actions.inventory_sources_with_failures.label} label={actions.inventory_sources_with_failures.label}
value={inventory.inventory_sources_with_failures} value={wsInventory.inventory_sources_with_failures}
helpText={actions.inventory_sources_with_failures.help_text} helpText={actions.inventory_sources_with_failures.help_text}
dataCy="constructed-inventory-sources-with-failures" dataCy="constructed-inventory-sources-with-failures"
/> />
@@ -204,26 +270,29 @@ function ConstructedInventoryDetail({ inventory }) {
/> />
<Detail <Detail
fullWidth fullWidth
label={t`Source Inventories`} label={t`Input Inventories`}
value={ value={
<ChipGroup <LabelGroup numLabels={5}>
numChips={5} {inputInventories?.map((inputInventory) => (
totalChips={sourceInventories?.length} <Label
ouiaId="source-inventory-chips" color="blue"
> key={inputInventory.id}
{sourceInventories?.map((sourceInventory) => ( render={({ className, content, componentRef }) => (
<Link <Link
key={sourceInventory.id} className={className}
to={`/inventories/inventory/${sourceInventory.id}/details`} innerRef={componentRef}
to={`/inventories/inventory/${inputInventory.id}/details`}
>
{content}
</Link>
)}
> >
<Chip key={sourceInventory.id} isReadOnly> {inputInventory.name}
{sourceInventory.name} </Label>
</Chip>
</Link>
))} ))}
</ChipGroup> </LabelGroup>
} }
isEmpty={sourceInventories?.length === 0} isEmpty={inputInventories?.length === 0}
/> />
<VariablesDetail <VariablesDetail
label={actions.source_vars.label} label={actions.source_vars.label}
@@ -245,7 +314,7 @@ function ConstructedInventoryDetail({ inventory }) {
/> />
</DetailList> </DetailList>
<CardActionsRow> <CardActionsRow>
{userCapabilities.edit && ( {inventory?.summary_fields?.user_capabilities?.edit && (
<Button <Button
ouiaId="inventory-detail-edit-button" ouiaId="inventory-detail-edit-button"
component={Link} component={Link}
@@ -254,7 +323,21 @@ function ConstructedInventoryDetail({ inventory }) {
{t`Edit`} {t`Edit`}
</Button> </Button>
)} )}
{userCapabilities.delete && ( {inventorySource?.summary_fields?.user_capabilities?.start &&
(['new', 'running', 'pending', 'waiting'].includes(
inventorySourceSyncJob?.status
) ? (
<JobCancelButton
job={{ id: inventorySourceSyncJob.id, type: 'inventory_update' }}
errorTitle={t`Constructed Inventory Source Sync Error`}
title={t`Cancel Constructed Inventory Source Sync`}
errorMessage={t`Failed to cancel Constructed Inventory Source Sync`}
buttonText={t`Cancel Sync`}
/>
) : (
<ConstructedInventorySyncButton inventoryId={inventory.id} />
))}
{inventory?.summary_fields?.user_capabilities?.delete && (
<DeleteButton <DeleteButton
name={inventory.name} name={inventory.name}
modalTitle={t`Delete Inventory`} modalTitle={t`Delete Inventory`}

View File

@@ -1,8 +1,17 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { Router } from 'react-router-dom';
import { InventoriesAPI, CredentialTypesAPI } from 'api'; import { InventoriesAPI, ConstructedInventoriesAPI } from 'api';
import {
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import '@testing-library/jest-dom';
import { createMemoryHistory } from 'history';
import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core';
import { en } from 'make-plural/plurals';
import english from '../../../locales/en/messages';
import ConstructedInventoryDetail from './ConstructedInventoryDetail'; import ConstructedInventoryDetail from './ConstructedInventoryDetail';
jest.mock('../../../api'); jest.mock('../../../api');
@@ -30,6 +39,15 @@ const mockInventory = {
copy: true, copy: true,
adhoc: true, adhoc: true,
}, },
labels: {
count: 1,
results: [
{
id: 17,
name: 'seventeen',
},
],
},
}, },
created: '2019-10-04T16:56:48.025455Z', created: '2019-10-04T16:56:48.025455Z',
modified: '2019-10-04T16:56:48.025468Z', modified: '2019-10-04T16:56:48.025468Z',
@@ -46,21 +64,187 @@ const mockInventory = {
total_inventory_sources: 0, total_inventory_sources: 0,
inventory_sources_with_failures: 0, inventory_sources_with_failures: 0,
pending_deletion: false, pending_deletion: false,
prevent_instance_group_fallback: false, prevent_instance_group_fallback: true,
update_cache_timeout: 0, update_cache_timeout: 0,
limit: '', limit: '',
verbosity: 1, verbosity: 1,
source_vars:
'{\n "plugin": "constructed",\n "strict": true,\n "groups": {\n "shutdown": "resolved_state == \\"shutdown\\"",\n "shutdown_in_product_dev": "resolved_state == \\"shutdown\\" and account_alias == \\"product_dev\\""\n },\n "compose": {\n "resolved_state": "state | default(\\"running\\")"\n }\n}',
}; };
describe('<ConstructedInventoryDetail />', () => { describe('<ConstructedInventoryDetail />', () => {
test('initially renders successfully', async () => { const history = createMemoryHistory({
let wrapper; initialEntries: ['/inventories/constructed_inventory/1/details'],
await act(async () => { });
wrapper = mountWithContexts(
<ConstructedInventoryDetail inventory={mockInventory} /> const Component = (props) => (
); <I18nProvider i18n={i18n}>
<Router history={history}>
<ConstructedInventoryDetail inventory={mockInventory} {...props} />
</Router>
</I18nProvider>
);
beforeEach(() => {
i18n.loadLocaleData({ en: { plurals: en } });
i18n.load({ en: english });
i18n.activate('en');
InventoriesAPI.readInstanceGroups.mockResolvedValue({
data: { results: [] },
}); });
expect(wrapper.length).toBe(1); InventoriesAPI.readInputInventories.mockResolvedValue({
expect(wrapper.find('ConstructedInventoryDetail').length).toBe(1); data: {
results: [
{
id: 123,
name: 'input_inventory_123',
},
{
id: 456,
name: 'input_inventory_456',
},
],
},
});
InventoriesAPI.readSources.mockResolvedValue({
data: {
results: [
{
id: 999,
type: 'inventory_source',
summary_fields: {
last_job: {
id: 101,
name: 'Auto-created source for: Constructed Inv',
status: 'successful',
finished: '2023-02-02T22:22:22.222220Z',
},
user_capabilities: {
start: true,
},
},
},
],
},
});
ConstructedInventoriesAPI.readOptions.mockResolvedValue({
data: {
related: {},
actions: {
GET: {
limit: {
label: 'Limit',
help_text: '',
},
total_groups: {
label: 'Total Groups',
help_text: '',
},
total_hosts: {
label: 'Total Hosts',
help_text: '',
},
total_inventory_sources: {
label: 'Total inventory sources',
help_text: '',
},
update_cache_timeout: {
label: 'Update cache timeout',
help_text: '',
},
inventory_sources_with_failures: {
label: 'Inventory sources with failures',
help_text: '',
},
source_vars: {
label: 'Source vars',
help_text: '',
},
verbosity: {
label: 'Verbosity',
help_text: '',
},
created: {
label: 'Created by',
help_text: '',
},
modified: {
label: 'Modified by',
help_text: '',
},
},
},
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('should render details', async () => {
render(<Component />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Constructed Inv')).toBeInTheDocument();
expect(screen.getByText('Last Job Status')).toBeInTheDocument();
expect(screen.getByText('Successful')).toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByText('Constructed Inventory')).toBeInTheDocument();
});
test('should render action buttons', async () => {
render(<Component />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(screen.getByRole('link', { name: 'Edit' })).toHaveAttribute(
'href',
'/inventories/constructed_inventory/1/edit'
);
expect(
screen.getByRole('button', { name: 'Start inventory source sync' })
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
});
test('should show cancel sync button during an inventory source sync running job', async () => {
InventoriesAPI.readSources.mockResolvedValue({
data: {
results: [
{
id: 999,
type: 'inventory_source',
summary_fields: {
current_job: {
id: 111,
name: 'Auto-created source for: Constructed Inv',
status: 'running',
},
user_capabilities: {
start: true,
},
},
},
],
},
});
render(<Component />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
screen.getByRole('button', {
name: 'Cancel Constructed Inventory Source Sync',
})
).toBeInTheDocument();
});
test('should show error when the api throws while fetching details', async () => {
InventoriesAPI.readInputInventories.mockRejectedValueOnce(new Error());
render(<Component />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
screen.getByText(
'There was an error loading this content. Please reload the page.'
)
).toBeInTheDocument();
}); });
}); });

View File

@@ -0,0 +1,59 @@
import React, { useCallback } from 'react';
import { t } from '@lingui/macro';
import PropTypes from 'prop-types';
import { Button, Tooltip } from '@patternfly/react-core';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import AlertModal from 'components/AlertModal/AlertModal';
import ErrorDetail from 'components/ErrorDetail/ErrorDetail';
import { InventoriesAPI } from 'api';
function ConstructedInventorySyncButton({ inventoryId }) {
const testId = `constructed-inventory-${inventoryId}-sync`;
const {
isLoading: startSyncLoading,
error: startSyncError,
request: startSyncProcess,
} = useRequest(
useCallback(
async () => InventoriesAPI.syncAllSources(inventoryId),
[inventoryId]
),
{}
);
const { error: startError, dismissError: dismissStartError } =
useDismissableError(startSyncError);
return (
<>
<Tooltip content={t`Start sync process`} position="top">
<Button
ouiaId={testId}
isDisabled={startSyncLoading}
aria-label={t`Start inventory source sync`}
variant="secondary"
onClick={startSyncProcess}
>
{t`Sync`}
</Button>
</Tooltip>
{startError && (
<AlertModal
isOpen={startError}
variant="error"
title={t`Error!`}
onClose={dismissStartError}
>
{t`Failed to sync constructed inventory source`}
<ErrorDetail error={startError} />
</AlertModal>
)}
</>
);
}
ConstructedInventorySyncButton.propTypes = {
inventoryId: PropTypes.number.isRequired,
};
export default ConstructedInventorySyncButton;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { InventoriesAPI } from 'api';
import ConstructedInventorySyncButton from './ConstructedInventorySyncButton';
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
jest.mock('../../../api');
const inventory = { id: 100, name: 'Constructed Inventory' };
describe('<ConstructedInventorySyncButton />', () => {
const Component = () => (
<ConstructedInventorySyncButton inventoryId={inventory.id} />
);
test('should render start sync button', () => {
render(<Component />);
expect(
screen.getByRole('button', { name: 'Start inventory source sync' })
).toBeInTheDocument();
});
test('should make expected api request on sync', async () => {
render(<Component />);
const syncButton = screen.queryByText('Sync');
fireEvent.click(syncButton);
await waitFor(() =>
expect(InventoriesAPI.syncAllSources).toHaveBeenCalledWith(100)
);
});
test('should show alert modal on throw', async () => {
InventoriesAPI.syncAllSources.mockRejectedValueOnce(new Error());
render(<Component />);
await waitFor(() => {
const syncButton = screen.queryByText('Sync');
fireEvent.click(syncButton);
});
expect(screen.getByRole('dialog', { name: 'Alert modal Error!' }));
});
});

View File

@@ -31,7 +31,7 @@ import { formatDateString } from 'util/dates';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import { VERBOSITY } from 'components/VerbositySelectField'; import { VERBOSITY } from 'components/VerbositySelectField';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails'; import useWsInventorySourcesDetails from '../shared/useWsInventorySourcesDetails';
import getHelpText from '../shared/Inventory.helptext'; import getHelpText from '../shared/Inventory.helptext';
function InventorySourceDetail({ inventorySource }) { function InventorySourceDetail({ inventorySource }) {

View File

@@ -1,42 +0,0 @@
import { useState, useEffect } from 'react';
import useWebsocket from 'hooks/useWebsocket';
export default function useWsInventorySourcesDetails(initialSources) {
const [sources, setSources] = useState(initialSources);
const lastMessage = useWebsocket({
jobs: ['status_changed'],
control: ['limit_reached_1'],
});
useEffect(() => {
setSources(initialSources);
}, [initialSources]);
useEffect(
() => {
if (
!lastMessage?.unified_job_id ||
!lastMessage?.inventory_source_id ||
lastMessage.type !== 'inventory_update'
) {
return;
}
const updateSource = {
...sources,
summary_fields: {
...sources.summary_fields,
current_job: {
id: lastMessage.unified_job_id,
status: lastMessage.status,
finished: lastMessage.finished,
},
},
};
setSources(updateSource);
},
[lastMessage] // eslint-disable-line react-hooks/exhaustive-deps
);
return sources;
}

View File

@@ -0,0 +1,58 @@
import { useState, useEffect } from 'react';
import useWebsocket from 'hooks/useWebsocket';
import { InventorySourcesAPI } from 'api';
export default function useWsInventorySourcesDetails(initialSource) {
const [source, setSource] = useState(initialSource);
const lastMessage = useWebsocket({
jobs: ['status_changed'],
control: ['limit_reached_1'],
});
useEffect(() => {
setSource(initialSource);
}, [initialSource]);
useEffect(
() => {
if (
!lastMessage?.unified_job_id ||
!lastMessage?.inventory_source_id ||
lastMessage.type !== 'inventory_update'
) {
return;
}
if (
['successful', 'failed', 'error', 'cancelled'].includes(
lastMessage.status
)
) {
fetchSource();
}
setSource(updateSource(source, lastMessage));
},
[lastMessage] // eslint-disable-line react-hooks/exhaustive-deps
);
async function fetchSource() {
const { data } = await InventorySourcesAPI.readDetail(source.id);
setSource(data);
}
return source;
}
function updateSource(source, message) {
return {
...source,
summary_fields: {
...source.summary_fields,
current_job: {
id: message.unified_job_id,
status: message.status,
finished: message.finished,
},
},
};
}

View File

@@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import WS from 'jest-websocket-mock'; import WS from 'jest-websocket-mock';
import { InventorySourcesAPI } from 'api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import useWsInventorySourceDetails from './useWsInventorySourcesDetails'; import useWsInventorySourceDetails from './useWsInventorySourcesDetails';
jest.mock('../../../api/models/InventorySources');
function TestInner() { function TestInner() {
return <div />; return <div />;
} }
@@ -111,6 +114,27 @@ describe('useWsProject', () => {
status: 'running', status: 'running',
finished: null, finished: null,
}); });
expect(InventorySourcesAPI.readDetail).toHaveBeenCalledTimes(0);
InventorySourcesAPI.readDetail.mockResolvedValue({
data: {},
});
await act(async () => {
mockServer.send(
JSON.stringify({
group_name: 'jobs',
inventory_id: 1,
status: 'successful',
type: 'inventory_update',
unified_job_id: 2,
unified_job_template_id: 1,
inventory_source_id: 1,
})
);
});
expect(InventorySourcesAPI.readDetail).toHaveBeenCalledTimes(1);
jest.clearAllMocks();
WS.clean(); WS.clean();
}); });
}); });