From cb501ac61329afd02fbae9c5a88ac41ef16a70e9 Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:11:20 +0100 Subject: [PATCH] [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 (cherry picked from commit 2ec351d99833bfe48fa25a10efc06c0ab66e33e6) --- .../use_is_osquery_available_simple.test.ts | 8 +- .../use_host_isolation_action.test.tsx | 94 +++++++++++-------- .../use_responder_action_data.test.ts | 63 +++++++------ .../mock/endpoint/app_context_render.tsx | 58 +++++++----- .../use_asset_criticality.test.ts | 16 ++-- .../components/artifact_flyout.test.tsx | 6 +- .../console_manager/console_manager.tsx | 10 +- .../console_manager.test.tsx | 56 ++++++----- .../hooks/agents/use_get_agent_status.test.ts | 45 ++++----- .../use_bulk_delete_artifact.test.tsx | 2 +- .../use_bulk_update_artifact.test.tsx | 2 +- .../artifacts/use_create_artifact.test.tsx | 2 +- .../artifacts/use_delete_artifact.test.tsx | 2 +- .../artifacts/use_get_updated_tags.test.tsx | 2 +- ..._host_isolation_exceptions_access.test.tsx | 14 +-- .../artifacts/use_update_artifact.test.tsx | 2 +- .../endpoint/use_get_endpoint_details.test.ts | 2 +- .../endpoint/use_get_endpoints_list.test.ts | 12 ++- .../hooks/endpoint/use_get_endpoints_list.ts | 2 +- .../policy/use_update_endpoint_policy.test.ts | 6 +- .../use_send_scan_request.test.ts | 4 +- .../public/management/hooks/test_utils.tsx | 8 +- 22 files changed, 232 insertions(+), 184 deletions(-) diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts index 77c91957b10dc..c00335e1a397b 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts @@ -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)); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/from_alerts/use_host_isolation_action.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/from_alerts/use_host_isolation_action.test.tsx index c5bc4a01f5140..bb7f50b066ed9 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/from_alerts/use_host_isolation_action.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/from_alerts/use_host_isolation_action.test.tsx @@ -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(); - expect(result.current).toEqual([ - buildExpectedMenuItemResult({ - ...(command === 'unisolate' ? { name: UNISOLATE_HOST } : {}), - }), - ]); + 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(); - expect(result.current).toEqual([ - buildExpectedMenuItemResult({ - disabled: true, - toolTipContent: LOADING_ENDPOINT_DATA_TOOLTIP, - }), - ]); + 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); - - expect(result.current).toEqual([ - buildExpectedMenuItemResult({ - disabled: true, - toolTipContent: - type === 'endpoint' ? HOST_ENDPOINT_UNENROLLED_TOOLTIP : NOT_FROM_ENDPOINT_HOST_TOOLTIP, - }), - ]); + 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, + }), + ]) + ); } ); 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]; + + 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); + }); - expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('unisolateHost'); + await appContextMock.waitFor(() => + expect(hookProps.onAddIsolationStatusClick).toHaveBeenCalledWith('unisolateHost') + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts index 3b68c0efdf9e6..6cbd0ced12387 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/responder/from_alerts/use_responder_action_data.test.ts @@ -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(); - result.current.handleResponseActionsClick(); + 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(); - result.current.handleResponseActionsClick(); + 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; + let renderHook: () => RenderHookResult; beforeEach(() => { endpointMetadataHttpMocks(appContextMock.coreStart.http); @@ -241,15 +245,13 @@ describe('use responder action data hooks', () => { onClick: onClickMock, }; renderHook = () => { - return appContextMock.renderHook(() => - 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); - result.current.handleResponseActionsClick(); + 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(); - result.current.handleResponseActionsClick(); + + act(() => { + result.current.handleResponseActionsClick(); + }); expect(onClickMock).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 23abdab4d14f9..1787856989141 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -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 = (props: TProps) => TResult; +type HookRendererFunction = (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, + hookFn: HookRendererFunction, /** * 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: ( + hookFn: HookRendererFunction, + options?: RenderHookOptions + ) => RenderHookResult; + + /** + * 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'] = ( - hookFn: HookRendererFunction, + const renderHook = ( + hookFn: HookRendererFunction, options: RenderHookOptions = {} - ): RenderHookResult => { - return reactRenderHook(hookFn, { - wrapper: AppWrapper as WrapperComponent, + ) => { + return reactRenderHook(hookFn, { + wrapper: AppWrapper as React.FC, ...options, }); }; @@ -319,16 +329,17 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { TProps, TResult extends UseBaseQueryResult = UseBaseQueryResult >( - hookFn: HookRendererFunction, + hookFn: HookRendererFunction, + /** + * If defined (default is `isSuccess`), the renderer will wait for the given react query to be truthy + */ waitForHook: WaitForReactHookState = 'isSuccess', options: RenderHookOptions = {} ) => { - const { result: hookResult, waitFor } = renderHook(hookFn, options); + const { result: hookResult } = renderHook(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, }; }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.test.ts index d4671a5bc628a..df9e39022f1e3 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.test.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.test.ts @@ -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({ diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx index 83b8682190101..66f372b7903b0 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx @@ -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; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/console_manager.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/console_manager.tsx index deb6967da2c27..7d85f5c11575c 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/console_manager.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/console_manager.tsx @@ -113,13 +113,9 @@ export const ConsoleManager = memo(({ 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 diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/integration_tests/console_manager.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/integration_tests/console_manager.test.tsx index 8cf7b94f9bcd8..912b96b4406cf 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/integration_tests/console_manager.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/integration_tests/console_manager.test.tsx @@ -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; + type RenderResultInterface = RenderHookResult; 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; + + 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,7 +169,9 @@ describe('When using ConsoleManager', () => { beforeEach(() => { renderHook(); ({ id: consoleId } = registerNewConsole()); - registeredConsole = renderResult.result.current.getOne(consoleId)!; + act(() => { + registeredConsole = renderResult.result.current.getOne(consoleId)!; + }); }); it('should have the expected interface', () => { @@ -178,27 +186,31 @@ describe('When using ConsoleManager', () => { }); it('should display the console when `.show()` is called', async () => { - registeredConsole.show(); - await renderResult.waitForNextUpdate(); - - expect(registeredConsole.isVisible()).toBe(true); + act(() => { + registeredConsole.show(); + }); + await waitFor(() => expect(registeredConsole.isVisible()).toBe(true)); }); it('should hide the console when `.hide()` is called', async () => { - registeredConsole.show(); - await renderResult.waitForNextUpdate(); - expect(registeredConsole.isVisible()).toBe(true); + act(() => { + registeredConsole.show(); + }); - registeredConsole.hide(); - await renderResult.waitForNextUpdate(); - expect(registeredConsole.isVisible()).toBe(false); + await waitFor(() => expect(registeredConsole.isVisible()).toBe(true)); + + act(() => { + registeredConsole.hide(); + }); + + await waitFor(() => expect(registeredConsole.isVisible()).toBe(false)); }); it('should un-register the console when `.terminate() is called', async () => { - registeredConsole.terminate(); - await renderResult.waitForNextUpdate(); - - expect(renderResult.result.current.getOne(consoleId)).toBeUndefined(); + act(() => { + registeredConsole.terminate(); + }); + await waitFor(() => expect(renderResult.result.current.getOne(consoleId)).toBeUndefined()); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.test.ts b/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.test.ts index 300fd11a700a3..ce8bb96a2c08c 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/agents/use_get_agent_status.test.ts @@ -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[0]; let optionsProp: Parameters[2]; let apiMock: ReturnType; - let renderHook: () => RenderHookResult>; + let renderHook: () => RenderHookResult, unknown>; beforeEach(() => { const appTestContext = createAppRootMockRenderer(); @@ -25,7 +26,7 @@ describe('useGetAgentStatus hook', () => { httpMock = appTestContext.coreStart.http; apiMock = agentStatusGetHttpMock(httpMock); renderHook = () => { - return appTestContext.renderHook>(() => + return appTestContext.renderHook(() => useGetAgentStatus(agentIdsProp, 'endpoint', optionsProp) ); }; @@ -63,28 +64,28 @@ describe('useGetAgentStatus hook', () => { }); it('should return expected data', async () => { - const { result, waitForValueToChange } = renderHook(); - await waitForValueToChange(() => result.current); - - expect(result.current.data).toEqual({ - '1-2-3': { - agentId: '1-2-3', - agentType: 'endpoint', - found: true, - isolated: false, - lastSeen: expect.any(String), - pendingActions: {}, - status: 'healthy', - }, - }); + const { result } = renderHook(); + await waitFor(() => + expect(result.current.data).toEqual({ + '1-2-3': { + agentId: '1-2-3', + agentType: 'endpoint', + found: true, + isolated: false, + lastSeen: expect.any(String), + 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); - - expect(result.current.data).toEqual({}); - expect(apiMock.responseProvider.getAgentStatus).not.toHaveBeenCalled(); + const { result } = renderHook(); + await waitFor(() => { + expect(result.current.data).toEqual({}); + expect(apiMock.responseProvider.getAgentStatus).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx index 403731566ef3f..2c6c6ff92003e 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_delete_artifact.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.test.tsx index 5db9734c61f9f..24f142773ae5e 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_bulk_update_artifact.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.test.tsx index 853ea0df96a3d..71d88d3732275 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_create_artifact.test.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx index 9473a50fa8a33..f988494bdceb7 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_delete_artifact.test.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_updated_tags.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_updated_tags.test.tsx index 0fb481a489c2f..1461f68fd1aaf 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_updated_tags.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_get_updated_tags.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx index 2bd673f68095f..8cf4c30cdda71 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_host_isolation_exceptions_access.test.tsx @@ -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); diff --git a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.test.tsx b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.test.tsx index 14607a33f2098..4c932812c8c01 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.test.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/artifacts/use_update_artifact.test.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_details.test.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_details.test.ts index 58222386da571..f96c7a42f6ceb 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_details.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_details.test.ts @@ -26,7 +26,7 @@ jest.mock('@tanstack/react-query', () => { describe('useGetEndpointDetails hook', () => { let renderReactQueryHook: ReactQueryHookRenderer< - Parameters, + Parameters[number], ReturnType >; let http: AppContextTestRender['coreStart']['http']; diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts index d7f073b2a8338..3c9ed815536df 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts @@ -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(() => diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.ts index 632b7d8ff48ae..b0072d73b832e 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.ts @@ -18,7 +18,7 @@ type GetEndpointsListResponse = Array<{ selected: boolean; }>; -const PAGING_PARAMS = Object.freeze({ +export const PAGING_PARAMS = Object.freeze({ default: 50, all: 10000, }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts index 8b6be08b41b46..6d27b5ccf447a 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.test.ts @@ -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; let policy: PolicyData; let renderHook: () => RenderHookResult< - UseUpdateEndpointPolicyOptions, - UseUpdateEndpointPolicyResult + UseUpdateEndpointPolicyResult, + UseUpdateEndpointPolicyOptions >; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_scan_request.test.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_scan_request.test.ts index f5b3f20d21d88..b6afc01e131a7 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_scan_request.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_send_scan_request.test.ts @@ -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; - let renderHook: () => RenderHookResult; + let renderHook: () => RenderHookResult; beforeEach(() => { const testContext = createAppRootMockRenderer(); diff --git a/x-pack/plugins/security_solution/public/management/hooks/test_utils.tsx b/x-pack/plugins/security_solution/public/management/hooks/test_utils.tsx index 149eee55872aa..cc674610bbaee 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/test_utils.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/test_utils.tsx @@ -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 => ( {children} ); - 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;