From 8327b1ae7abd2d6c178db1a366061a691310af2e Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:11:47 +0100 Subject: [PATCH] [React18] Migrate test suites to account for testing library upgrades kibana-security (#201151) 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 f0540977af8b552cf9eae747c04cf37f3e01c742) --- .../hooks/use_update_user_profile.test.tsx | 18 +++++----- .../form_components/src/form_changes.test.tsx | 2 +- .../components/case_form_fields/tags.test.tsx | 5 ++- .../user_profile/user_profile.test.tsx | 2 +- .../public/components/use_badge.test.tsx | 2 +- .../components/use_capabilities.test.tsx | 2 +- .../edit_space_content_tab.test.tsx | 2 +- .../provider/edit_space_provider.test.tsx | 15 +++----- .../space_assign_role_privilege_form.test.tsx | 36 +++++++++---------- 9 files changed, 41 insertions(+), 43 deletions(-) diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx index 87d41bd222637..c802ce41bdb57 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act, renderHook, type WrapperComponent } from '@testing-library/react-hooks'; +import { act, renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { BehaviorSubject, first, lastValueFrom, of } from 'rxjs'; @@ -35,7 +35,7 @@ const security = { const { http, notifications } = core; -const wrapper: WrapperComponent> = ({ children }) => ( +const wrapper = ({ children }: React.PropsWithChildren) => ( { await lastValueFrom(updateDone.pipe(first((v) => v === true))); }); - const { result, waitForNextUpdate } = renderHook(() => useUpdateUserProfile(), { wrapper }); + const { result } = renderHook(() => useUpdateUserProfile(), { wrapper }); const { update } = result.current; expect(result.current.isLoading).toBeFalsy(); @@ -90,9 +90,7 @@ describe('useUpdateUserProfile() hook', () => { expect(result.current.isLoading).toBeTruthy(); updateDone.next(true); // Resolve the http.post promise - await waitForNextUpdate(); - - expect(result.current.isLoading).toBeFalsy(); + await waitFor(() => expect(result.current.isLoading).toBeFalsy()); }); test('should show a success notification by default', async () => { @@ -118,7 +116,9 @@ describe('useUpdateUserProfile() hook', () => { return true; }; - const { result } = renderHook(() => useUpdateUserProfile({ pageReloadChecker }), { wrapper }); + const { result } = renderHook(() => useUpdateUserProfile({ pageReloadChecker }), { + wrapper, + }); const { update } = result.current; await act(async () => { @@ -146,7 +146,9 @@ describe('useUpdateUserProfile() hook', () => { userProfile$: of(initialValue), }; - const { result } = renderHook(() => useUpdateUserProfile({ pageReloadChecker }), { wrapper }); + const { result } = renderHook(() => useUpdateUserProfile({ pageReloadChecker }), { + wrapper, + }); const { update } = result.current; const nextValue = { userSettings: { darkMode: 'light' as const } }; diff --git a/x-pack/packages/security/form_components/src/form_changes.test.tsx b/x-pack/packages/security/form_components/src/form_changes.test.tsx index 3223bb727ddfb..1ce5e4d1e07de 100644 --- a/x-pack/packages/security/form_components/src/form_changes.test.tsx +++ b/x-pack/packages/security/form_components/src/form_changes.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react'; import type { RevertFunction } from './form_changes'; import { useFormChanges } from './form_changes'; diff --git a/x-pack/plugins/cases/public/components/case_form_fields/tags.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/tags.test.tsx index 914993648e29a..9b92baa30bb79 100644 --- a/x-pack/plugins/cases/public/components/case_form_fields/tags.test.tsx +++ b/x-pack/plugins/cases/public/components/case_form_fields/tags.test.tsx @@ -47,11 +47,14 @@ describe('Tags', () => { }; beforeEach(() => { - jest.clearAllMocks(); useGetTagsMock.mockReturnValue({ data: ['test'] }); appMockRender = createAppMockRenderer(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('it renders', async () => { appMockRender.render( diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx index 01d6fc95afcc1..3c349b36fefb5 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react'; import { mount } from 'enzyme'; import type { FC, PropsWithChildren } from 'react'; import React from 'react'; diff --git a/x-pack/plugins/security/public/components/use_badge.test.tsx b/x-pack/plugins/security/public/components/use_badge.test.tsx index 07d0261235301..643ebeffa8991 100644 --- a/x-pack/plugins/security/public/components/use_badge.test.tsx +++ b/x-pack/plugins/security/public/components/use_badge.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import React from 'react'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/x-pack/plugins/security/public/components/use_capabilities.test.tsx b/x-pack/plugins/security/public/components/use_capabilities.test.tsx index 5e8491199a5da..8a5a8eb3ebda8 100644 --- a/x-pack/plugins/security/public/components/use_capabilities.test.tsx +++ b/x-pack/plugins/security/public/components/use_capabilities.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import React from 'react'; import { coreMock } from '@kbn/core/public/mocks'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx index 3e7e04c18a7b8..6f668b79756c8 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx @@ -114,7 +114,7 @@ describe('EditSpaceContentTab', () => { ); - await waitFor(() => null); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(getSpaceContentSpy).toHaveBeenCalledTimes(1); expect(getSpaceContentSpy).toHaveBeenCalledWith(space.id); diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx index 9e091514f7df5..4da9806b0dee0 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { PropsWithChildren } from 'react'; import React from 'react'; @@ -88,10 +88,8 @@ describe('EditSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useEditSpaceServices); - expect(result.error).toBeDefined(); - expect(result.error?.message).toEqual( - expect.stringMatching('EditSpaceService Context is missing.') + expect(() => renderHook(useEditSpaceServices)).toThrow( + /EditSpaceService Context is missing./ ); }); }); @@ -109,12 +107,7 @@ describe('EditSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useEditSpaceStore); - - expect(result.error).toBeDefined(); - expect(result.error?.message).toEqual( - expect.stringMatching('EditSpaceStore Context is missing.') - ); + expect(() => renderHook(useEditSpaceStore)).toThrow(/EditSpaceStore Context is missing./); }); }); }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index d35bdbee4b8bb..5b1e263e20f16 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -157,7 +157,7 @@ describe('PrivilegesRolesForm', () => { renderPrivilegeRolesForm(); - await waitFor(() => null); + await waitFor(() => new Promise((resolve) => resolve(null))); ['all', 'read', 'custom'].forEach((privilege) => { expect(screen.queryByTestId(`${privilege}-privilege-button`)).not.toBeInTheDocument(); @@ -174,9 +174,9 @@ describe('PrivilegesRolesForm', () => { renderPrivilegeRolesForm(); - await waitFor(() => null); - - expect(screen.getByTestId('space-assign-role-create-roles-privilege-button')).toBeDisabled(); + await waitFor(() => + expect(screen.getByTestId('space-assign-role-create-roles-privilege-button')).toBeDisabled() + ); }); it('makes a request to refetch available roles if page transitions back from a user interaction page visibility change', () => { @@ -208,7 +208,7 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( 'aria-pressed', @@ -234,11 +234,11 @@ describe('PrivilegesRolesForm', () => { ], }); - await waitFor(() => null); - - expect(screen.getByTestId(`${FEATURE_PRIVILEGES_ALL}-privilege-button`)).toHaveAttribute( - 'aria-pressed', - String(true) + await waitFor(() => + expect(screen.getByTestId(`${FEATURE_PRIVILEGES_ALL}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(true) + ) ); }); @@ -256,7 +256,7 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); + await waitFor(() => new Promise((resolve) => resolve(null))); expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( 'aria-pressed', @@ -290,9 +290,9 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); - - expect(screen.getByTestId('privilege-conflict-callout')).toBeInTheDocument(); + await waitFor(() => + expect(screen.getByTestId('privilege-conflict-callout')).toBeInTheDocument() + ); }); it('does not display the permission conflict message when roles with the same privilege levels are selected', async () => { @@ -312,9 +312,9 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); - - expect(screen.queryByTestId('privilege-conflict-callout')).not.toBeInTheDocument(); + await waitFor(() => + expect(screen.queryByTestId('privilege-conflict-callout')).not.toBeInTheDocument() + ); }); }); @@ -348,7 +348,7 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); + await waitFor(() => new Promise((resolve) => resolve(null))); await userEvent.click(screen.getByTestId('custom-privilege-button'));