[React18] Migrate test suites to account for testing library upgrades security-defend-workflows (#201174)

This PR migrates test suites that use `renderHook` from the library
`@testing-library/react-hooks` to adopt the equivalent and replacement
of `renderHook` from the export that is now available from
`@testing-library/react`. This work is required for the planned
migration to react18.

##  Context

In this PR, usages of `waitForNextUpdate` that previously could have
been destructured from `renderHook` are now been replaced with `waitFor`
exported from `@testing-library/react`, furthermore `waitFor`
that would also have been destructured from the same renderHook result
is now been replaced with `waitFor` from the export of
`@testing-library/react`.

***Why is `waitFor` a sufficient enough replacement for
`waitForNextUpdate`, and better for testing values subject to async
computations?***

WaitFor will retry the provided callback if an error is returned, till
the configured timeout elapses. By default the retry interval is `50ms`
with a timeout value of `1000ms` that
effectively translates to at least 20 retries for assertions placed
within waitFor. See
https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
for more information.
This however means that for person's writing tests, said person has to
be explicit about expectations that describe the internal state of the
hook being tested.
This implies checking for instance when a react query hook is being
rendered, there's an assertion that said hook isn't loading anymore.

In this PR you'd notice that this pattern has been adopted, with most
existing assertions following an invocation of `waitForNextUpdate` being
placed within a `waitFor`
invocation. In some cases the replacement is simply a `waitFor(() => new
Promise((resolve) => resolve(null)))` (many thanks to @kapral18, for
point out exactly why this works),
where this suffices the assertions that follow aren't placed within a
waitFor so this PR doesn't get larger than it needs to be.

It's also worth pointing out this PR might also contain changes to test
and application code to improve said existing test.

### What to do next?
1. Review the changes in this PR.
2. If you think the changes are correct, approve the PR.

## Any questions?
If you have any questions or need help with this PR, please leave
comments in this PR.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Eyo O. Eyo 2024-11-27 13:11:20 +01:00 committed by GitHub
parent 6e5f5ed3bd
commit 2ec351d998
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 232 additions and 184 deletions

View file

@ -6,7 +6,7 @@
*/
import { useKibana } from '../../common/lib/kibana';
import { useIsOsqueryAvailableSimple } from './use_is_osquery_available_simple';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, waitFor } from '@testing-library/react';
import { createStartServicesMock } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react.mock';
import { OSQUERY_INTEGRATION_NAME } from '../../../common';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -41,15 +41,13 @@ describe('UseIsOsqueryAvailableSimple', () => {
});
});
it('should expect response from API and return enabled flag', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useIsOsqueryAvailableSimple({
agentId: '3242332',
})
);
expect(result.current).toBe(false);
await waitForValueToChange(() => result.current);
expect(result.current).toBe(true);
await waitFor(() => expect(result.current).toBe(true));
});
});

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type React from 'react';
import { act } from '@testing-library/react';
import type { UseHostIsolationActionProps } from './use_host_isolation_action';
import { useHostIsolationAction } from './use_host_isolation_action';
import type { AppContextTestRender, UserPrivilegesMockSetter } from '../../../../mock/endpoint';
@ -15,7 +16,6 @@ import type { AlertTableContextMenuItem } from '../../../../../detections/compon
import type { ResponseActionsApiCommandNames } from '../../../../../../common/endpoint/service/response_actions/constants';
import { agentStatusMocks } from '../../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
import type React from 'react';
import {
HOST_ENDPOINT_UNENROLLED_TOOLTIP,
LOADING_ENDPOINT_DATA_TOOLTIP,
@ -87,20 +87,22 @@ describe('useHostIsolationAction', () => {
});
}
const { result, waitForValueToChange } = render();
await waitForValueToChange(() => result.current);
const { result } = render();
await appContextMock.waitFor(() =>
expect(result.current).toEqual([
buildExpectedMenuItemResult({
...(command === 'unisolate' ? { name: UNISOLATE_HOST } : {}),
}),
]);
])
);
}
);
it('should call `closePopover` callback when menu item `onClick` is called', async () => {
const { result, waitForValueToChange } = render();
await waitForValueToChange(() => result.current);
const { result } = render();
await appContextMock.waitFor(() => expect(result.current[0].onClick).toBeDefined());
result.current[0].onClick!({} as unknown as React.MouseEvent);
expect(hookProps.closePopover).toHaveBeenCalled();
@ -135,12 +137,14 @@ describe('useHostIsolationAction', () => {
it('should return disabled menu item while loading agent status', async () => {
const { result } = render();
await appContextMock.waitFor(() =>
expect(result.current).toEqual([
buildExpectedMenuItemResult({
disabled: true,
toolTipContent: LOADING_ENDPOINT_DATA_TOOLTIP,
}),
]);
])
);
});
it.each(['endpoint', 'non-endpoint'])(
@ -156,37 +160,51 @@ describe('useHostIsolationAction', () => {
if (type === 'non-endpoint') {
hookProps.detailsData = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
}
const { result, waitForValueToChange } = render();
await waitForValueToChange(() => result.current);
const { result } = render();
await appContextMock.waitFor(() =>
expect(result.current).toEqual([
buildExpectedMenuItemResult({
disabled: true,
toolTipContent:
type === 'endpoint' ? HOST_ENDPOINT_UNENROLLED_TOOLTIP : NOT_FROM_ENDPOINT_HOST_TOOLTIP,
type === 'endpoint'
? HOST_ENDPOINT_UNENROLLED_TOOLTIP
: NOT_FROM_ENDPOINT_HOST_TOOLTIP,
}),
]);
])
);
}
);
it('should call isolate API when agent is currently NOT isolated', async () => {
const { result, waitForValueToChange } = render();
await waitForValueToChange(() => result.current);
const { result } = render();
await appContextMock.waitFor(() => expect(result.current[0].onClick).toBeDefined());
result.current[0].onClick!({} as unknown as React.MouseEvent);
expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('isolateHost');
});
it('should call un-isolate API when agent is currently isolated', async () => {
apiMock.responseProvider.getAgentStatus.mockReturnValue(
agentStatusMocks.generateAgentStatusApiResponse({
data: { 'abfe4a35-d5b4-42a0-a539-bd054c791769': { isolated: true } },
})
);
const { result, waitForValueToChange } = render();
await waitForValueToChange(() => result.current);
result.current[0].onClick!({} as unknown as React.MouseEvent);
apiMock.responseProvider.getAgentStatus.mockImplementation(({ query }) => {
const agentId = (query!.agentIds as string[])[0];
expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('unisolateHost');
return agentStatusMocks.generateAgentStatusApiResponse({
data: { [agentId]: { isolated: true } },
});
});
const { result } = render();
await appContextMock.waitFor(() => {
expect(apiMock.responseProvider.getAgentStatus).toHaveBeenCalled();
expect(result.current[0].onClick).toBeDefined();
});
act(() => {
result.current[0].onClick!({} as unknown as React.MouseEvent);
});
await appContextMock.waitFor(() =>
expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('unisolateHost')
);
});
});

View file

@ -25,7 +25,8 @@ import type { AppContextTestRender } from '../../../../mock/endpoint';
import { createAppRootMockRenderer, endpointAlertDataMock } from '../../../../mock/endpoint';
import { HOST_METADATA_LIST_ROUTE } from '../../../../../../common/endpoint/constants';
import { endpointMetadataHttpMocks } from '../../../../../management/pages/endpoint_hosts/mocks';
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
import type { RenderHookResult } from '@testing-library/react';
import { waitFor, act } from '@testing-library/react';
import { createHttpFetchError } from '@kbn/core-http-browser-mocks';
import { HostStatus } from '../../../../../../common/endpoint/types';
import {
@ -61,17 +62,14 @@ describe('use responder action data hooks', () => {
describe('useWithResponderActionDataFromAlert() hook', () => {
let renderHook: () => RenderHookResult<
UseWithResponderActionDataFromAlertProps,
ResponderActionData
ResponderActionData,
UseWithResponderActionDataFromAlertProps
>;
let alertDetailItemData: TimelineEventsDetailsItem[];
beforeEach(() => {
renderHook = () => {
return appContextMock.renderHook<
UseWithResponderActionDataFromAlertProps,
ResponderActionData
>(() =>
return appContextMock.renderHook(() =>
useWithResponderActionDataFromAlert({
eventData: alertDetailItemData,
onClick: onClickMock,
@ -95,7 +93,9 @@ describe('use responder action data hooks', () => {
it('should call `onClick()` function prop when is pass to the hook', () => {
alertDetailItemData = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
const { result } = renderHook();
act(() => {
result.current.handleResponseActionsClick();
});
expect(onClickMock).toHaveBeenCalled();
});
@ -103,7 +103,9 @@ describe('use responder action data hooks', () => {
it('should NOT call `onClick` if the action is disabled', () => {
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType('foo');
const { result } = renderHook();
act(() => {
result.current.handleResponseActionsClick();
});
expect(onClickMock).not.toHaveBeenCalled();
});
@ -169,8 +171,8 @@ describe('use responder action data hooks', () => {
});
it('should show action enabled if host metadata was retrieved and host is enrolled', async () => {
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.isDisabled);
const { result } = renderHook();
await waitFor(() => expect(result.current.isDisabled).toBe(false));
expect(result.current).toEqual(getExpectedResponderActionData());
});
@ -181,8 +183,10 @@ describe('use responder action data hooks', () => {
statusCode: 404,
});
});
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.tooltip);
const { result } = renderHook();
await waitFor(() => expect(result.current.tooltip).not.toEqual('Loading'));
expect(result.current).toEqual(
getExpectedResponderActionData({
@ -199,8 +203,8 @@ describe('use responder action data hooks', () => {
};
metadataApiMocks.responseProvider.metadataDetails.mockReturnValue(hostMetadata);
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.tooltip);
const { result } = renderHook();
await waitFor(() => expect(result.current.tooltip).not.toEqual('Loading'));
expect(result.current).toEqual(
getExpectedResponderActionData({
@ -216,8 +220,8 @@ describe('use responder action data hooks', () => {
statusCode: 500,
});
});
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.tooltip);
const { result } = renderHook();
await waitFor(() => expect(result.current.tooltip).not.toEqual('Loading'));
expect(result.current).toEqual(
getExpectedResponderActionData({
@ -231,7 +235,7 @@ describe('use responder action data hooks', () => {
describe('useResponderActionData() hook', () => {
let hookProps: UseResponderActionDataProps;
let renderHook: () => RenderHookResult<UseResponderActionDataProps, ResponderActionData>;
let renderHook: () => RenderHookResult<ResponderActionData, UseResponderActionDataProps>;
beforeEach(() => {
endpointMetadataHttpMocks(appContextMock.coreStart.http);
@ -241,15 +245,13 @@ describe('use responder action data hooks', () => {
onClick: onClickMock,
};
renderHook = () => {
return appContextMock.renderHook<UseResponderActionDataProps, ResponderActionData>(() =>
useResponderActionData(hookProps)
);
return appContextMock.renderHook(() => useResponderActionData(hookProps));
};
});
it('should show action enabled when agentType is Endpoint and host is enabled', async () => {
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.isDisabled);
const { result } = renderHook();
await waitFor(() => expect(result.current.isDisabled).toBe(false));
expect(result.current).toEqual(getExpectedResponderActionData());
});
@ -266,9 +268,13 @@ describe('use responder action data hooks', () => {
});
it('should call `onClick` prop when action is enabled', async () => {
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current.isDisabled);
const { result } = renderHook();
await waitFor(() => expect(result.current.isDisabled).toBe(false));
act(() => {
result.current.handleResponseActionsClick();
});
expect(onClickMock).toHaveBeenCalled();
});
@ -276,7 +282,10 @@ describe('use responder action data hooks', () => {
it('should not call `onCLick` prop when action is disabled', () => {
hookProps.agentType = 'sentinel_one';
const { result } = renderHook();
act(() => {
result.current.handleResponseActionsClick();
});
expect(onClickMock).not.toHaveBeenCalled();
});

View file

@ -9,18 +9,21 @@ import type { ReactPortal } from 'react';
import React from 'react';
import type { MemoryHistory } from 'history';
import { createMemoryHistory } from 'history';
import type { RenderOptions, RenderResult } from '@testing-library/react';
import { render as reactRender } from '@testing-library/react';
import type {
RenderOptions,
RenderResult,
RenderHookResult,
RenderHookOptions,
} from '@testing-library/react';
import {
render as reactRender,
waitFor,
renderHook as reactRenderHook,
} from '@testing-library/react';
import type { Action, Reducer, Store } from 'redux';
import { QueryClient } from '@tanstack/react-query';
import { coreMock } from '@kbn/core/public/mocks';
import { PLUGIN_ID } from '@kbn/fleet-plugin/common';
import type { RenderHookOptions, RenderHookResult } from '@testing-library/react-hooks';
import { renderHook as reactRenderHook } from '@testing-library/react-hooks';
import type {
ReactHooksRenderer,
WrapperComponent,
} from '@testing-library/react-hooks/src/types/react';
import type { UseBaseQueryResult } from '@tanstack/react-query';
import ReactDOM from 'react-dom';
import type { DeepReadonly } from 'utility-types';
@ -101,17 +104,16 @@ export type WaitForReactHookState =
>
| false;
type HookRendererFunction<TProps, TResult> = (props: TProps) => TResult;
type HookRendererFunction<TResult, TProps> = (props: TProps) => TResult;
/**
* A utility renderer for hooks that return React Query results
*/
export type ReactQueryHookRenderer<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TProps = any,
TProps = unknown,
TResult extends UseBaseQueryResult = UseBaseQueryResult
> = (
hookFn: HookRendererFunction<TProps, TResult>,
hookFn: HookRendererFunction<TResult, TProps>,
/**
* If defined (default is `isSuccess`), the renderer will wait for the given react
* query response state value to be true
@ -150,7 +152,15 @@ export interface AppContextTestRender {
/**
* Renders a hook within a mocked security solution app context
*/
renderHook: ReactHooksRenderer['renderHook'];
renderHook: <TResult, TProps>(
hookFn: HookRendererFunction<TResult, TProps>,
options?: RenderHookOptions<TProps>
) => RenderHookResult<TResult, TProps>;
/**
* Waits the return value of the callback provided to is truthy
*/
waitFor: typeof waitFor;
/**
* A helper utility for rendering specifically hooks that wrap ReactQuery
@ -305,12 +315,12 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
});
};
const renderHook: ReactHooksRenderer['renderHook'] = <TProps, TResult>(
hookFn: HookRendererFunction<TProps, TResult>,
const renderHook = <TResult, TProps>(
hookFn: HookRendererFunction<TResult, TProps>,
options: RenderHookOptions<TProps> = {}
): RenderHookResult<TProps, TResult> => {
return reactRenderHook<TProps, TResult>(hookFn, {
wrapper: AppWrapper as WrapperComponent<TProps>,
) => {
return reactRenderHook<TResult, TProps>(hookFn, {
wrapper: AppWrapper as React.FC<React.PropsWithChildren>,
...options,
});
};
@ -319,16 +329,17 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
TProps,
TResult extends UseBaseQueryResult = UseBaseQueryResult
>(
hookFn: HookRendererFunction<TProps, TResult>,
hookFn: HookRendererFunction<TResult, TProps>,
/**
* If defined (default is `isSuccess`), the renderer will wait for the given react query to be truthy
*/
waitForHook: WaitForReactHookState = 'isSuccess',
options: RenderHookOptions<TProps> = {}
) => {
const { result: hookResult, waitFor } = renderHook<TProps, TResult>(hookFn, options);
const { result: hookResult } = renderHook<TResult, TProps>(hookFn, options);
if (waitForHook) {
await waitFor(() => {
return hookResult.current[waitForHook];
});
await waitFor(() => expect(hookResult.current[waitForHook]).toBe(true));
}
return hookResult.current;
@ -400,6 +411,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
setExperimentalFlag,
getUserPrivilegesMockSetter,
queryClient,
waitFor,
};
};

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { renderMutation, renderQuery } from '../../../management/hooks/test_utils';
import {
renderMutation,
renderQuery,
renderWrappedHook,
} from '../../../management/hooks/test_utils';
import type { Entity } from './use_asset_criticality';
import { useAssetCriticalityPrivileges, useAssetCriticalityData } from './use_asset_criticality';
@ -69,10 +73,7 @@ describe('useAssetCriticality', () => {
mockCreateAssetCriticality.mockResolvedValue({});
const entity: Entity = { name: 'test_entity_name', type: 'host' };
const { mutation } = await renderQuery(
() => useAssetCriticalityData({ entity }),
'isSuccess'
);
const { mutation } = await renderWrappedHook(() => useAssetCriticalityData({ entity }));
await renderMutation(async () =>
mutation.mutate({
@ -91,10 +92,7 @@ describe('useAssetCriticality', () => {
mockCreateAssetCriticality.mockResolvedValue({});
const entity: Entity = { name: 'test_entity_name', type: 'host' };
const { mutation } = await renderQuery(
() => useAssetCriticalityData({ entity }),
'isSuccess'
);
const { mutation } = await renderWrappedHook(() => useAssetCriticalityData({ entity }));
await renderMutation(async () =>
mutation.mutate({

View file

@ -47,9 +47,9 @@ describe('When the flyout is opened in the ArtifactListPage component', () => {
render = async (props = {}) => {
renderResult = renderSetup.renderArtifactListPage(props);
await waitFor(async () => {
expect(renderResult.getByTestId('testPage-flyout'));
});
await waitFor(async () =>
expect(renderResult.getByTestId('testPage-flyout')).toBeInTheDocument()
);
return renderResult;
};

View file

@ -113,13 +113,9 @@ export const ConsoleManager = memo<ConsoleManagerProps>(({ storage = {}, childre
validateIdOrThrow(id);
setConsoleStorage((prevState) => {
return {
...prevState,
[id]: {
...prevState[id],
isOpen: false,
},
};
const newState = { ...prevState };
newState[id].isOpen = false;
return newState;
});
},
[validateIdOrThrow] // << IMPORTANT: this callback should have only immutable dependencies

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import type { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook as _renderHook, act } from '@testing-library/react-hooks';
import { useConsoleManager } from '../console_manager';
import React from 'react';
import type {
@ -22,12 +20,13 @@ import {
getNewConsoleRegistrationMock,
} from '../mocks';
import userEvent, { type UserEvent } from '@testing-library/user-event';
import { waitFor } from '@testing-library/react';
import type { RenderHookResult } from '@testing-library/react';
import { waitFor, act, renderHook as _renderHook } from '@testing-library/react';
import { enterConsoleCommand } from '../../../mocks';
describe('When using ConsoleManager', () => {
describe('and using the ConsoleManagerInterface via the hook', () => {
type RenderResultInterface = RenderHookResult<never, ConsoleManagerClient>;
type RenderResultInterface = RenderHookResult<ConsoleManagerClient, never>;
let renderHook: () => RenderResultInterface;
let renderResult: RenderResultInterface;
@ -103,20 +102,27 @@ describe('When using ConsoleManager', () => {
);
});
it('should hide a console by `id`', () => {
it('should hide a console by `id`', async () => {
renderHook();
const { id: consoleId } = registerNewConsole();
let consoleClient: ReturnType<ConsoleManagerClient['getOne']>;
act(() => {
consoleClient = renderResult.result.current.getOne(consoleId);
});
act(() => {
renderResult.result.current.show(consoleId);
});
expect(renderResult.result.current.getOne(consoleId)!.isVisible()).toBe(true);
await waitFor(() => expect(consoleClient!.isVisible()).toBe(true));
act(() => {
renderResult.result.current.hide(consoleId);
});
expect(renderResult.result.current.getOne(consoleId)!.isVisible()).toBe(false);
await waitFor(() => expect(consoleClient!.isVisible()).toBe(false));
});
it('should throw if attempting to hide a console with invalid `id`', () => {
@ -163,8 +169,10 @@ describe('When using ConsoleManager', () => {
beforeEach(() => {
renderHook();
({ id: consoleId } = registerNewConsole());
act(() => {
registeredConsole = renderResult.result.current.getOne(consoleId)!;
});
});
it('should have the expected interface', () => {
expect(registeredConsole).toEqual({
@ -178,27 +186,31 @@ describe('When using ConsoleManager', () => {
});
it('should display the console when `.show()` is called', async () => {
act(() => {
registeredConsole.show();
await renderResult.waitForNextUpdate();
expect(registeredConsole.isVisible()).toBe(true);
});
await waitFor(() => expect(registeredConsole.isVisible()).toBe(true));
});
it('should hide the console when `.hide()` is called', async () => {
act(() => {
registeredConsole.show();
await renderResult.waitForNextUpdate();
expect(registeredConsole.isVisible()).toBe(true);
});
await waitFor(() => expect(registeredConsole.isVisible()).toBe(true));
act(() => {
registeredConsole.hide();
await renderResult.waitForNextUpdate();
expect(registeredConsole.isVisible()).toBe(false);
});
await waitFor(() => expect(registeredConsole.isVisible()).toBe(false));
});
it('should un-register the console when `.terminate() is called', async () => {
act(() => {
registeredConsole.terminate();
await renderResult.waitForNextUpdate();
expect(renderResult.result.current.getOne(consoleId)).toBeUndefined();
});
await waitFor(() => expect(renderResult.result.current.getOne(consoleId)).toBeUndefined());
});
});
});

View file

@ -10,14 +10,15 @@ import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { useGetAgentStatus } from './use_get_agent_status';
import { agentStatusGetHttpMock } from '../../mocks';
import { AGENT_STATUS_ROUTE } from '../../../../common/endpoint/constants';
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
import type { RenderHookResult } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
describe('useGetAgentStatus hook', () => {
let httpMock: AppContextTestRender['coreStart']['http'];
let agentIdsProp: Parameters<typeof useGetAgentStatus>[0];
let optionsProp: Parameters<typeof useGetAgentStatus>[2];
let apiMock: ReturnType<typeof agentStatusGetHttpMock>;
let renderHook: () => RenderHookResult<unknown, ReturnType<typeof useGetAgentStatus>>;
let renderHook: () => RenderHookResult<ReturnType<typeof useGetAgentStatus>, unknown>;
beforeEach(() => {
const appTestContext = createAppRootMockRenderer();
@ -25,7 +26,7 @@ describe('useGetAgentStatus hook', () => {
httpMock = appTestContext.coreStart.http;
apiMock = agentStatusGetHttpMock(httpMock);
renderHook = () => {
return appTestContext.renderHook<unknown, ReturnType<typeof useGetAgentStatus>>(() =>
return appTestContext.renderHook(() =>
useGetAgentStatus(agentIdsProp, 'endpoint', optionsProp)
);
};
@ -63,9 +64,8 @@ describe('useGetAgentStatus hook', () => {
});
it('should return expected data', async () => {
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current);
const { result } = renderHook();
await waitFor(() =>
expect(result.current.data).toEqual({
'1-2-3': {
agentId: '1-2-3',
@ -76,15 +76,16 @@ describe('useGetAgentStatus hook', () => {
pendingActions: {},
status: 'healthy',
},
});
})
);
});
it('should NOT call agent status api if list of agent ids is empty', async () => {
agentIdsProp = ['', ' '];
const { result, waitForValueToChange } = renderHook();
await waitForValueToChange(() => result.current);
const { result } = renderHook();
await waitFor(() => {
expect(result.current.data).toEqual({});
expect(apiMock.responseProvider.getAgentStatus).not.toHaveBeenCalled();
});
});
});

View file

@ -15,7 +15,7 @@ import {
renderMutation,
} from '../test_utils';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
const apiVersion = '2023-10-31';

View file

@ -15,7 +15,7 @@ import {
renderMutation,
} from '../test_utils';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
const apiVersion = '2023-10-31';

View file

@ -15,7 +15,7 @@ import {
renderMutation,
} from '../test_utils';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
describe('Create artifact hook', () => {
let result: ReturnType<typeof useCreateArtifact>;

View file

@ -15,7 +15,7 @@ import {
renderMutation,
} from '../test_utils';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
describe('Delete artifact hook', () => {
let result: ReturnType<typeof useDeleteArtifact>;

View file

@ -6,7 +6,7 @@
*/
import { useGetUpdatedTags } from '.';
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import type { TagFilter } from '../../../../common/endpoint/service/artifacts/utils';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, waitFor } from '@testing-library/react';
import { useHostIsolationExceptionsAccess } from './use_host_isolation_exceptions_access';
import { checkArtifactHasData } from '../../services/exceptions_list/check_artifact_has_data';
@ -29,7 +29,7 @@ describe('useHostIsolationExceptionsAccess', () => {
};
test('should set access to true if canAccessHostIsolationExceptions is true', async () => {
const { result, waitFor } = setupHook(true, false);
const { result } = setupHook(true, false);
await waitFor(() => expect(result.current.hasAccessToHostIsolationExceptions).toBe(true));
});
@ -37,7 +37,7 @@ describe('useHostIsolationExceptionsAccess', () => {
test('should check for artifact data if canReadHostIsolationExceptions is true and canAccessHostIsolationExceptions is false', async () => {
mockArtifactHasData();
const { result, waitFor } = setupHook(false, true);
const { result } = setupHook(false, true);
await waitFor(() => {
expect(checkArtifactHasData).toHaveBeenCalledWith(mockApiClient());
@ -48,7 +48,7 @@ describe('useHostIsolationExceptionsAccess', () => {
test('should set access to false if canReadHostIsolationExceptions is true but no artifact data exists', async () => {
mockArtifactHasData(false);
const { result, waitFor } = setupHook(false, true);
const { result } = setupHook(false, true);
await waitFor(() => {
expect(checkArtifactHasData).toHaveBeenCalledWith(mockApiClient());
@ -57,14 +57,14 @@ describe('useHostIsolationExceptionsAccess', () => {
});
test('should set access to false if neither canAccessHostIsolationExceptions nor canReadHostIsolationExceptions is true', async () => {
const { result, waitFor } = setupHook(false, false);
const { result } = setupHook(false, false);
await waitFor(() => {
expect(result.current.hasAccessToHostIsolationExceptions).toBe(false);
});
});
test('should not call checkArtifactHasData if canAccessHostIsolationExceptions is true', async () => {
const { result, waitFor } = setupHook(true, true);
const { result } = setupHook(true, true);
await waitFor(() => {
expect(checkArtifactHasData).not.toHaveBeenCalled();
@ -73,7 +73,7 @@ describe('useHostIsolationExceptionsAccess', () => {
});
test('should set loading state correctly while checking access', async () => {
const { result, waitFor } = setupHook(false, true);
const { result } = setupHook(false, true);
expect(result.current.isHostIsolationExceptionsAccessLoading).toBe(true);

View file

@ -15,7 +15,7 @@ import {
renderMutation,
} from '../test_utils';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
describe('Update artifact hook', () => {
let result: ReturnType<typeof useUpdateArtifact>;

View file

@ -27,7 +27,7 @@ jest.mock('@tanstack/react-query', () => {
// FLAKY: https://github.com/elastic/kibana/issues/192435
describe.skip('useGetEndpointDetails hook', () => {
let renderReactQueryHook: ReactQueryHookRenderer<
Parameters<typeof useGetEndpointDetails>,
Parameters<typeof useGetEndpointDetails>[number],
ReturnType<typeof useGetEndpointDetails>
>;
let http: AppContextTestRender['coreStart']['http'];

View file

@ -7,7 +7,7 @@
import type { AppContextTestRender, ReactQueryHookRenderer } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { useGetEndpointsList } from './use_get_endpoints_list';
import { useGetEndpointsList, PAGING_PARAMS } from './use_get_endpoints_list';
import { HOST_METADATA_LIST_ROUTE } from '../../../../common/endpoint/constants';
import { useQuery as _useQuery } from '@tanstack/react-query';
import { endpointMetadataHttpMocks } from '../../pages/endpoint_hosts/mocks';
@ -117,13 +117,15 @@ describe('useGetEndpointsList hook', () => {
it('should also list inactive agents', async () => {
const getApiResponse = apiMocks.responseProvider.metadataList.getMockImplementation();
const inActiveIndex = [0, 1, 3];
// set a few of the agents as inactive/unenrolled
apiMocks.responseProvider.metadataList.mockImplementation(() => {
if (getApiResponse) {
return {
...getApiResponse(),
data: getApiResponse().data.map((item, i) => {
const isInactiveIndex = [0, 1, 3].includes(i);
const isInactiveIndex = inActiveIndex.includes(i);
return {
...item,
host_status: isInactiveIndex ? HostStatus.INACTIVE : item.host_status,
@ -154,7 +156,7 @@ describe('useGetEndpointsList hook', () => {
const res = await renderReactQueryHook(() => useGetEndpointsList({ searchString: 'inactive' }));
expect(
res.data?.map((host) => host.name.split('-')[2]).filter((name) => name === 'inactive').length
).toEqual(3);
).toEqual(inActiveIndex.length);
});
it('should only list 50 agents when more than 50 in the metadata list API', async () => {
@ -192,7 +194,7 @@ describe('useGetEndpointsList hook', () => {
// verify useGetEndpointsList hook returns all 50 agents in the list
const res = await renderReactQueryHook(() => useGetEndpointsList({ searchString: '' }));
expect(res.data?.length).toEqual(50);
expect(res.data?.length).toEqual(PAGING_PARAMS.default);
});
it('should only list 10 more agents when 50 or more agents are already selected', async () => {
@ -232,7 +234,7 @@ describe('useGetEndpointsList hook', () => {
const agentIdsToSelect = apiMocks.responseProvider
.metadataList()
.data.map((d) => d.metadata.agent.id)
.slice(0, 50);
.slice(0, PAGING_PARAMS.default);
// call useGetEndpointsList with all 50 agents selected
const res = await renderReactQueryHook(() =>

View file

@ -18,7 +18,7 @@ type GetEndpointsListResponse = Array<{
selected: boolean;
}>;
const PAGING_PARAMS = Object.freeze({
export const PAGING_PARAMS = Object.freeze({
default: 50,
all: 10000,
});

View file

@ -13,7 +13,7 @@ import type {
UseUpdateEndpointPolicyOptions,
UseUpdateEndpointPolicyResult,
} from './use_update_endpoint_policy';
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
import type { RenderHookResult } from '@testing-library/react';
import { useUpdateEndpointPolicy } from './use_update_endpoint_policy';
import type { PolicyData } from '../../../../common/endpoint/types';
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
@ -37,8 +37,8 @@ describe('When using the `useFetchEndpointPolicyAgentSummary()` hook', () => {
let apiMocks: ReturnType<typeof allFleetHttpMocks>;
let policy: PolicyData;
let renderHook: () => RenderHookResult<
UseUpdateEndpointPolicyOptions,
UseUpdateEndpointPolicyResult
UseUpdateEndpointPolicyResult,
UseUpdateEndpointPolicyOptions
>;
beforeEach(() => {

View file

@ -7,7 +7,7 @@
import { useMutation as _useMutation } from '@tanstack/react-query';
import type { AppContextTestRender } from '../../../common/mock/endpoint';
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
import type { RenderHookResult } from '@testing-library/react';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import {
@ -38,7 +38,7 @@ describe('When using the `useSendScanRequest()` hook', () => {
let customOptions: ScanRequestCustomOptions;
let http: AppContextTestRender['coreStart']['http'];
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let renderHook: () => RenderHookResult<ScanRequestCustomOptions, UseSendScanRequestResult>;
let renderHook: () => RenderHookResult<UseSendScanRequestResult, ScanRequestCustomOptions>;
beforeEach(() => {
const testContext = createAppRootMockRenderer();

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { waitFor, renderHook } from '@testing-library/react';
import type { HttpSetup } from '@kbn/core/public';
import type { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { coreMock } from '@kbn/core/public/mocks';
@ -39,10 +39,10 @@ export const renderQuery = async (
const wrapper = ({ children }: { children: React.ReactNode }): JSX.Element => (
<ReactQueryClientProvider>{children}</ReactQueryClientProvider>
);
const { result: resultHook, waitFor } = renderHook(() => hook(), {
const { result: resultHook } = renderHook(() => hook(), {
wrapper,
});
await waitFor(() => resultHook.current[waitForHook]);
await waitFor(() => expect(resultHook.current[waitForHook]).toBeTruthy());
return resultHook.current;
};
@ -58,3 +58,5 @@ export const renderMutation = async (
});
return resultHook.current;
};
export const renderWrappedHook = renderMutation;