diff --git a/src/platform/packages/shared/kbn-management/settings/application/__snapshots__/query_input.test.tsx.snap b/src/platform/packages/shared/kbn-management/settings/application/__snapshots__/query_input.test.tsx.snap index 84fff87de365d..86e6a754bf394 100644 --- a/src/platform/packages/shared/kbn-management/settings/application/__snapshots__/query_input.test.tsx.snap +++ b/src/platform/packages/shared/kbn-management/settings/application/__snapshots__/query_input.test.tsx.snap @@ -1,59 +1,82 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Search should render normally 1`] = ` - - - +
+
+
+
+
+
+ + +
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
`; diff --git a/src/platform/packages/shared/kbn-management/settings/application/query_input.test.tsx b/src/platform/packages/shared/kbn-management/settings/application/query_input.test.tsx index 5a36535b8eee8..cfbc56bb2634b 100644 --- a/src/platform/packages/shared/kbn-management/settings/application/query_input.test.tsx +++ b/src/platform/packages/shared/kbn-management/settings/application/query_input.test.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { Query } from '@elastic/eui'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { act, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import { shallowWithI18nProvider, mountWithI18nProvider } from '@kbn/test-jest-helpers'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settings.mock'; import { QueryInput } from './query_input'; @@ -27,57 +27,54 @@ const categories = Object.keys( ) ); +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + describe('Search', () => { it('should render normally', async () => { const onQueryChange = () => {}; - const component = shallowWithI18nProvider( - - ); + const { container } = renderWithI18n(); - expect(component).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should call parent function when query is changed', async () => { - // This test is brittle as it knows about implementation details - // (EuiFieldSearch uses onKeyup instead of onChange to handle input) const onQueryChange = jest.fn(); - const component = mountWithI18nProvider( - - ); - findTestSubject(component, 'settingsSearchBar').simulate('keyup', { - target: { value: 'new filter' }, - }); - expect(onQueryChange).toHaveBeenCalledTimes(1); + renderWithI18n(); + + const searchBar = screen.getByTestId('settingsSearchBar'); + await user.type(searchBar, 'new filter'); + + expect(onQueryChange).toHaveBeenCalled(); }); it('should handle query parse error', async () => { const onQueryChange = jest.fn(); - const component = mountWithI18nProvider( - - ); + renderWithI18n(); - const searchBar = findTestSubject(component, 'settingsSearchBar'); + const searchBar = screen.getByTestId('settingsSearchBar'); // Send invalid query - act(() => { - searchBar.simulate('keyup', { target: { value: '?' } }); - }); + await user.type(searchBar, '?'); - expect(onQueryChange).toHaveBeenCalledTimes(1); - - waitFor(() => { - expect(component.contains('Unable to parse query')).toBe(true); - }); + expect(onQueryChange).toHaveBeenCalled(); + expect(screen.getByText(/Unable to parse query/)).toBeInTheDocument(); // Send valid query to ensure component can recover from invalid query - act(() => { - searchBar.simulate('keyup', { target: { value: 'dateFormat' } }); - }); - - expect(onQueryChange).toHaveBeenCalledTimes(2); + await user.clear(searchBar); + await user.type(searchBar, 'dateFormat'); - waitFor(() => { - expect(component.contains('Unable to parse query')).toBe(false); - }); + expect(screen.queryByText(/Unable to parse query/)).not.toBeInTheDocument(); }); }); diff --git a/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap b/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap index c1fbeab0538e5..03368d4e46b8b 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap +++ b/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CronEditor is rendered with a DAY frequency 1`] = ` -Array [ +
- , +
- , -] + + `; exports[`CronEditor is rendered with a HOUR frequency 1`] = ` -Array [ +
- , +
- , -] + + `; exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` -
+
- -
-
-
- -
+ Frequency + +
+
- + Every + +
- + + + + + + + +
+ class="euiFormControlLayoutCustomIcon emotion-euiFormControlLayoutCustomIcon" + > + +
@@ -1165,7 +1167,7 @@ exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` `; exports[`CronEditor is rendered with a MONTH frequency 1`] = ` -Array [ +
-
, +
- , +
- , -] + + `; exports[`CronEditor is rendered with a WEEK frequency 1`] = ` -Array [ +
- , +
- , +
- , -] + + `; exports[`CronEditor is rendered with a YEAR frequency 1`] = ` -Array [ +
- , +
- , +
- , +
- , -] + + `; diff --git a/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx b/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx index d5fd3d89283c7..c82a09a877290 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx +++ b/src/platform/plugins/shared/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx @@ -8,9 +8,10 @@ */ import React from 'react'; -import sinon from 'sinon'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithI18nProvider } from '@kbn/test-jest-helpers'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { renderWithI18n } from '@kbn/test-jest-helpers'; import type { Frequency } from './types'; import { CronEditor } from './cron_editor'; @@ -18,7 +19,7 @@ import { CronEditor } from './cron_editor'; describe('CronEditor', () => { ['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => { test(`is rendered with a ${unit} frequency`, () => { - const component = mountWithI18nProvider( + const { container } = renderWithI18n( { /> ); - expect(component.render()).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); }); describe('props', () => { describe('frequencyBlockList', () => { it('excludes the blocked frequencies from the frequency list', () => { - const component = mountWithI18nProvider( + renderWithI18n( { /> ); - const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); - expect(frequencySelect.text()).toBe('minutedaymonth'); + const frequencySelect = screen.getByTestId('cronFrequencySelect'); + expect(frequencySelect).toHaveTextContent('minutedaymonth'); }); }); describe('cronExpression', () => { it('sets the values of the fields', () => { - const component = mountWithI18nProvider( + renderWithI18n( { /> ); - const monthSelect = findTestSubject(component, 'cronFrequencyYearlyMonthSelect'); - expect(monthSelect.props().value).toBe('2'); + const monthSelect = screen.getByTestId( + 'cronFrequencyYearlyMonthSelect' + ) as HTMLSelectElement; + expect(monthSelect.value).toBe('2'); - const dateSelect = findTestSubject(component, 'cronFrequencyYearlyDateSelect'); - expect(dateSelect.props().value).toBe('5'); + const dateSelect = screen.getByTestId('cronFrequencyYearlyDateSelect') as HTMLSelectElement; + expect(dateSelect.value).toBe('5'); - const hourSelect = findTestSubject(component, 'cronFrequencyYearlyHourSelect'); - expect(hourSelect.props().value).toBe('10'); + const hourSelect = screen.getByTestId('cronFrequencyYearlyHourSelect') as HTMLSelectElement; + expect(hourSelect.value).toBe('10'); - const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); - expect(minuteSelect.props().value).toBe('20'); + const minuteSelect = screen.getByTestId( + 'cronFrequencyYearlyMinuteSelect' + ) as HTMLSelectElement; + expect(minuteSelect.value).toBe('20'); }); }); describe('onChange', () => { - it('is called when the frequency changes', () => { - const onChangeSpy = sinon.spy(); - const component = mountWithI18nProvider( + it('is called when the frequency changes', async () => { + const user = userEvent.setup(); + const onChangeSpy = jest.fn(); + renderWithI18n( { /> ); - const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); - frequencySelect.simulate('change', { target: { value: 'MONTH' } }); + const frequencySelect = screen.getByTestId('cronFrequencySelect'); + await user.selectOptions(frequencySelect, 'MONTH'); - sinon.assert.calledWith(onChangeSpy, { + expect(onChangeSpy).toHaveBeenCalledWith({ cronExpression: '0 0 0 1 * ?', fieldToPreferredValueMap: {}, frequency: 'MONTH', }); }); - it(`is called when a field's value changes`, () => { - const onChangeSpy = sinon.spy(); - const component = mountWithI18nProvider( + it(`is called when a field's value changes`, async () => { + const user = userEvent.setup(); + const onChangeSpy = jest.fn(); + renderWithI18n( { /> ); - const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); - minuteSelect.simulate('change', { target: { value: '40' } }); + const minuteSelect = screen.getByTestId('cronFrequencyYearlyMinuteSelect'); + await user.selectOptions(minuteSelect, '40'); - sinon.assert.calledWith(onChangeSpy, { + expect(onChangeSpy).toHaveBeenCalledWith({ cronExpression: '0 40 * * * ?', fieldToPreferredValueMap: { minute: '40' }, frequency: 'YEAR', diff --git a/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap b/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap index ba5da3e6850d5..5dcbebe69c830 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap +++ b/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap @@ -1,127 +1,129 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ViewApiRequestFlyout is rendered 1`] = ` -
- - -
-
-
-

+ +

+
+
- Request code block - -
-
-              
-                Hello world
-              
-            
+

+ Request code block +

+
+
+                
+                  Hello world
+                
+              
+
- -
- + +
`; diff --git a/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx b/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx index 0e3a7f37b7490..384f44d1d023f 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx +++ b/src/platform/plugins/shared/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx @@ -8,11 +8,12 @@ */ import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mountWithI18nProvider } from '@kbn/test-jest-helpers'; -import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { compressToEncodedURIComponent } from 'lz-string'; +import { renderWithI18n } from '@kbn/test-jest-helpers'; + import { ViewApiRequestFlyout } from './view_api_request_flyout'; import type { UrlService } from '@kbn/share-plugin/common/url_service'; import type { ApplicationStart } from '@kbn/core/public'; @@ -46,35 +47,35 @@ const applicationMock = { describe('ViewApiRequestFlyout', () => { test('is rendered', () => { - const component = mountWithI18nProvider(); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); + const { container } = renderWithI18n(); + expect(container).toMatchSnapshot(); }); describe('props', () => { test('on closeFlyout', async () => { - const component = mountWithI18nProvider(); + const user = userEvent.setup(); + renderWithI18n(); - await act(async () => { - findTestSubject(component, 'apiRequestFlyoutClose').simulate('click'); - }); + const closeButton = screen.getByTestId('apiRequestFlyoutClose'); + await user.click(closeButton); - expect(payload.closeFlyout).toBeCalled(); + expect(payload.closeFlyout).toHaveBeenCalled(); }); test('doesnt have openInConsole when some optional props are not supplied', async () => { - const component = mountWithI18nProvider(); + renderWithI18n(); - const openInConsole = findTestSubject(component, 'apiRequestFlyoutOpenInConsoleButton'); - expect(openInConsole.length).toEqual(0); + const openInConsole = screen.queryByTestId('apiRequestFlyoutOpenInConsoleButton'); + expect(openInConsole).not.toBeInTheDocument(); // Flyout should *not* be wrapped with RedirectAppLinks - const redirectWrapper = findTestSubject(component, 'apiRequestFlyoutRedirectWrapper'); - expect(redirectWrapper.length).toEqual(0); + const redirectWrapper = screen.queryByTestId('apiRequestFlyoutRedirectWrapper'); + expect(redirectWrapper).not.toBeInTheDocument(); }); test('has openInConsole when all optional props are supplied', async () => { const encodedRequest = compressToEncodedURIComponent(payload.request); - const component = mountWithI18nProvider( + renderWithI18n( { /> ); - const openInConsole = findTestSubject(component, 'apiRequestFlyoutOpenInConsoleButton'); - expect(openInConsole.length).toEqual(1); - expect(openInConsole.props().href).toEqual(`devToolsUrl_data:text/plain,${encodedRequest}`); + const openInConsole = screen.getByTestId('apiRequestFlyoutOpenInConsoleButton'); + expect(openInConsole).toBeInTheDocument(); + expect(openInConsole).toHaveAttribute( + 'href', + `devToolsUrl_data:text/plain,${encodedRequest}` + ); // Flyout should be wrapped with RedirectAppLinks - const redirectWrapper = findTestSubject(component, 'apiRequestFlyoutRedirectWrapper'); - expect(redirectWrapper.length).toEqual(1); + const redirectWrapper = screen.getByTestId('apiRequestFlyoutRedirectWrapper'); + expect(redirectWrapper).toBeInTheDocument(); }); }); }); diff --git a/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.helpers.tsx index c376768dc3901..a07d00b18b404 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.helpers.tsx +++ b/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -8,9 +8,7 @@ */ import React, { useState, useEffect } from 'react'; -import { act } from 'react-dom/test-utils'; -import type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; +import { renderHook, act } from '@testing-library/react'; import sinon from 'sinon'; import type { HttpSetup, HttpFetchOptions } from '@kbn/core/public'; @@ -19,7 +17,6 @@ import type { UseRequestResponse, UseRequestConfig } from './use_request'; import { useRequest } from './use_request'; export interface UseRequestHelpers { - advanceTime: (ms: number) => Promise; completeRequest: () => Promise; hookResult: UseRequestResponse; getSendRequestSpy: () => sinon.SinonStub; @@ -30,10 +27,13 @@ export interface UseRequestHelpers { setErrorResponse: (overrides?: {}) => void; setupErrorWithBodyRequest: (overrides?: {}) => void; getErrorWithBodyResponse: () => SendRequestResponse; + teardown: () => Promise; // For real timers + teardownFake: () => Promise; // For fake timers } -// Each request will take 1s to resolve. -export const REQUEST_TIME = 1000; +// Short delay for tests using real timers +// Using 50ms to provide enough buffer for real timer async operations +export const REQUEST_TIME = 50; const successRequest: SendRequestConfig = { method: 'post', path: '/success', body: {} }; const successResponse = { statusCode: 200, data: { message: 'Success message' } }; @@ -52,28 +52,17 @@ const errorWithBodyResponse = { body: errorValue }; export const createUseRequestHelpers = (): UseRequestHelpers => { // The behavior we're testing involves state changes over time, so we need finer control over // timing. - jest.useFakeTimers({ legacyFakeTimers: true }); - - const flushPromiseJobQueue = async () => { - // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function - await Promise.resolve(); - }; + jest.useFakeTimers(); const completeRequest = async () => { await act(async () => { - jest.runAllTimers(); - await flushPromiseJobQueue(); - }); - }; - - const advanceTime = async (ms: number) => { - await act(async () => { - jest.advanceTimersByTime(ms); - await flushPromiseJobQueue(); + await jest.runAllTimersAsync(); }); }; - let element: ReactWrapper; + let hookReturn: ReturnType< + typeof renderHook + >; // We'll use this object to observe the state of the hook and access its callback(s). const hookResult = {} as UseRequestResponse; const sendRequestSpy = sinon.stub(); @@ -97,31 +86,28 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { }, }; - const TestComponent = ({ requestConfig }: { requestConfig: UseRequestConfig }) => { - const { isInitialRequest, isLoading, error, data, resendRequest } = useRequest( - httpClient as HttpSetup, - requestConfig - ); - - // Force a re-render of the component to stress-test the useRequest hook and verify its + const Wrapper = ({ children }: { children?: React.ReactNode }) => { + // Force a re-render to stress-test the useRequest hook and verify its // state remains unaffected. const [, setState] = useState(false); useEffect(() => { setState(true); }, []); - - hookResult.isInitialRequest = isInitialRequest; - hookResult.isLoading = isLoading; - hookResult.error = error; - hookResult.data = data; - hookResult.resendRequest = resendRequest; - - return null; + return <>{children}; }; - act(() => { - element = mount(); - }); + hookReturn = renderHook( + ({ requestConfig }) => { + const result = useRequest(httpClient as HttpSetup, requestConfig); + // Sync the result to hookResult object for compatibility with existing tests + Object.assign(hookResult, result); + return result; + }, + { + initialProps: { requestConfig: config }, + wrapper: Wrapper, + } + ); }; // Set up successful request helpers. @@ -156,7 +142,7 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { }); // We'll use this to change a success response to an error response, to test how the state changes. const setErrorResponse = (overrides = {}) => { - element.setProps({ requestConfig: { ...errorRequest, ...overrides } }); + hookReturn.rerender({ requestConfig: { ...errorRequest, ...overrides } }); }; // Set up failed request helpers with the alternative error shape. @@ -176,8 +162,22 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { error: errorWithBodyResponse.body, }); + const teardownFake = async () => { + hookReturn?.unmount(); + await jest.runOnlyPendingTimersAsync(); + jest.clearAllTimers(); + jest.useRealTimers(); + sendRequestSpy.resetHistory(); + }; + + const teardown = async () => { + hookReturn?.unmount(); + // Wait briefly for any pending real timers to complete + await new Promise((resolve) => setTimeout(resolve, 20)); + sendRequestSpy.resetHistory(); + }; + return { - advanceTime, completeRequest, hookResult, getSendRequestSpy: () => sendRequestSpy, @@ -188,5 +188,7 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { setErrorResponse, setupErrorWithBodyRequest, getErrorWithBodyResponse, + teardown, + teardownFake, }; }; diff --git a/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.ts b/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.ts index 56e81e0eb21b0..35ba2864df0b6 100644 --- a/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.ts +++ b/src/platform/plugins/shared/es_ui_shared/public/request/use_request.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { act } from 'react-dom/test-utils'; +import { act, waitFor } from '@testing-library/react'; import sinon from 'sinon'; import type { UseRequestHelpers } from './use_request.test.helpers'; @@ -22,6 +22,10 @@ describe('useRequest hook', () => { describe('parameters', () => { describe('path, method, body', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); + it('is used to send the request', async () => { const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; setupSuccessRequest(); @@ -31,24 +35,32 @@ describe('useRequest hook', () => { }); describe('pollIntervalMs', () => { + afterEach(async () => { + await helpers.teardown(); + }); + it('sends another request after the specified time has elapsed', async () => { - const { setupSuccessRequest, advanceTime, getSendRequestSpy } = helpers; + jest.useRealTimers(); // Use real timers to avoid fake timer + act() state batching issues + const { setupSuccessRequest, getSendRequestSpy } = helpers; + setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); - await advanceTime(REQUEST_TIME); - expect(getSendRequestSpy().callCount).toBe(1); + // Wait for first request to complete + await waitFor(() => expect(getSendRequestSpy().callCount).toBe(1)); - // We need to advance (1) the pollIntervalMs and (2) the request time. - await advanceTime(REQUEST_TIME * 2); - expect(getSendRequestSpy().callCount).toBe(2); + // Wait for second request (poll fires + request completes) + await waitFor(() => expect(getSendRequestSpy().callCount).toBe(2)); - // We need to advance (1) the pollIntervalMs and (2) the request time. - await advanceTime(REQUEST_TIME * 2); - expect(getSendRequestSpy().callCount).toBe(3); + // Wait for third request (poll fires + request completes) + await waitFor(() => expect(getSendRequestSpy().callCount).toBe(3)); }); }); describe('initialData', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); + it('sets the initial data value', async () => { const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; setupSuccessRequest({ initialData: 'initialData' }); @@ -61,6 +73,10 @@ describe('useRequest hook', () => { }); describe('deserializer', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); + it('is called with the response once the request resolves', async () => { const { setupSuccessRequest, completeRequest, getSuccessResponse } = helpers; @@ -83,6 +99,10 @@ describe('useRequest hook', () => { }); describe('state', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); + describe('isInitialRequest', () => { it('is true for the first request and false for subsequent requests', async () => { const { setupSuccessRequest, completeRequest, hookResult } = helpers; @@ -200,6 +220,10 @@ describe('useRequest hook', () => { describe('callbacks', () => { describe('resendRequest', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); + it('sends the request', async () => { const { setupSuccessRequest, completeRequest, hookResult, getSendRequestSpy } = helpers; setupSuccessRequest(); @@ -215,33 +239,48 @@ describe('useRequest hook', () => { }); it('resets the pollIntervalMs', async () => { - const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + const { setupSuccessRequest, hookResult, getSendRequestSpy } = helpers; const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; setupSuccessRequest({ pollIntervalMs: DOUBLE_REQUEST_TIME }); - // The initial request resolves, and then we'll immediately send a new one manually... - await advanceTime(REQUEST_TIME); + await act(async () => { + // Advance time enough for the initial request to complete + await jest.advanceTimersByTimeAsync(DOUBLE_REQUEST_TIME + REQUEST_TIME); + }); + + // Wait for initial request to complete, then send a manual one expect(getSendRequestSpy().callCount).toBe(1); act(() => { hookResult.resendRequest(); }); - // The manual request resolves, and we'll send yet another one... - await advanceTime(REQUEST_TIME); + await act(async () => { + // Advance time enough for the manual request to complete + await jest.advanceTimersByTimeAsync(DOUBLE_REQUEST_TIME + REQUEST_TIME); + }); + + // Wait for manual request to complete, then send another manual one expect(getSendRequestSpy().callCount).toBe(2); act(() => { hookResult.resendRequest(); }); - // At this point, we've moved forward 3s. The poll is set at 2s. If resendRequest didn't - // reset the poll, the request call count would be 4, not 3. - await advanceTime(REQUEST_TIME); + await act(async () => { + // Advance time enough for the second manual request to complete + await jest.advanceTimersByTimeAsync(DOUBLE_REQUEST_TIME + REQUEST_TIME); + }); + + // Wait for the second manual request to complete + // If resendRequest didn't reset the poll, we would see 4 requests instead of 3 expect(getSendRequestSpy().callCount).toBe(3); }); }); }); describe('request behavior', () => { + afterEach(async () => { + await helpers.teardownFake(); + }); it('outdated responses are ignored by poll requests', async () => { const { setupSuccessRequest, @@ -270,22 +309,31 @@ describe('useRequest hook', () => { }); it(`outdated responses are ignored if there's a more recently-sent manual request`, async () => { - const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + const { setupSuccessRequest, hookResult, getSendRequestSpy } = helpers; - const HALF_REQUEST_TIME = REQUEST_TIME * 0.5; setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); - // Before the original request resolves, we make a manual resendRequest call. - await advanceTime(HALF_REQUEST_TIME); + // Wait half the request time - the initial request hasn't completed yet + await act(async () => { + await jest.advanceTimersByTimeAsync(REQUEST_TIME * 0.5); + }); expect(getSendRequestSpy().callCount).toBe(0); + + // Make a manual resendRequest call before the original resolves act(() => { hookResult.resendRequest(); }); - // The original quest resolves but it's been marked as outdated by the the manual resendRequest - // call "interrupts", so data is left undefined. - await advanceTime(HALF_REQUEST_TIME); + // Wait for the original request to complete - give it enough time + buffer + // Original started at T=0, will complete at T=REQUEST_TIME + // We're now at T=REQUEST_TIME*0.5, so wait REQUEST_TIME*0.6 more + await act(async () => { + await jest.advanceTimersByTimeAsync(REQUEST_TIME * 0.5); + }); + + // The spy should have been called once (original request completed) expect(getSendRequestSpy().callCount).toBe(1); + // But the result was ignored, so data should still be undefined expect(hookResult.data).toBeUndefined(); }); diff --git a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index ca58ea985ca00..874de7881606c 100644 --- a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -8,20 +8,24 @@ */ import React, { useState } from 'react'; -import { act } from 'react-dom/test-utils'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import type { TestBed } from '../shared_imports'; -import { registerTestBed } from '../shared_imports'; import type { OnUpdateHandler } from '../types'; import { useForm } from '../hooks/use_form'; import { Form } from './form'; import { UseField } from './use_field'; import { FormDataProvider } from './form_data_provider'; +const user = userEvent.setup(); +const onFormData = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); +}); + describe('', () => { test('should listen to changes in the form data and re-render the children with the updated data', async () => { - const onFormData = jest.fn(); - const TestComp = () => { const { form } = useForm(); @@ -40,48 +44,35 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { - form: { setInputValue }, - } = setup() as TestBed; - - expect(onFormData.mock.calls.length).toBe(1); + render(); - const [formDataInitial] = onFormData.mock.calls[ - onFormData.mock.calls.length - 1 - ] as Parameters; + expect(onFormData).toBeCalledTimes(1); - expect(formDataInitial).toEqual({ + expect(onFormData).toHaveBeenCalledWith({ name: 'Initial value', lastName: 'Initial value', }); - onFormData.mockReset(); // Reset the counter at 0 + onFormData.mockClear(); // Make some changes to the form fields - await act(async () => { - setInputValue('nameField', 'updated value'); - setInputValue('lastNameField', 'updated value'); - }); - - expect(onFormData).toBeCalledTimes(2); - - const [formDataUpdated] = onFormData.mock.calls[ - onFormData.mock.calls.length - 1 - ] as Parameters; - - expect(formDataUpdated).toEqual({ - name: 'updated value', - lastName: 'updated value', + const nameField = screen.getByTestId('nameField'); + const lastNameField = screen.getByTestId('lastNameField'); + + await user.clear(nameField); + await user.type(nameField, 'updated name'); + await user.clear(lastNameField); + await user.type(lastNameField, 'updated lastname'); + + // userEvent.type() triggers onChange for each character typed + // The important thing is to verify the final form data is correct + expect(onFormData).toHaveBeenCalledWith({ + name: 'updated name', + lastName: 'updated lastname', }); }); test('should subscribe to the latest updated form data when mounting late', async () => { - const onFormData = jest.fn(); - const TestComp = () => { const { form } = useForm(); const [isOn, setIsOn] = useState(false); @@ -104,41 +95,27 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { - form: { setInputValue }, - find, - } = setup() as TestBed; + render(); expect(onFormData).toBeCalledTimes(0); // Not present in the DOM yet // Make some changes to the form fields - await act(async () => { - setInputValue('nameField', 'updated value'); - }); + const nameField = screen.getByTestId('nameField'); + await user.clear(nameField); + await user.type(nameField, 'updated value'); // Update state to trigger the mounting of the FormDataProvider - await act(async () => { - find('btn').simulate('click').update(); - }); - - expect(onFormData.mock.calls.length).toBe(2); + const button = screen.getByTestId('btn'); + await user.click(button); - const [formDataUpdated] = onFormData.mock.calls[ - onFormData.mock.calls.length - 1 - ] as Parameters; + expect(onFormData).toHaveBeenCalledTimes(2); - expect(formDataUpdated).toEqual({ + expect(onFormData).toHaveBeenCalledWith({ name: 'updated value', }); }); test('props.pathsToWatch (string): should not re-render the children when the field that changed is not the one provided', async () => { - const onFormData = jest.fn(); - const TestComp = () => { const { form } = useForm(); @@ -156,27 +133,19 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { - form: { setInputValue }, - } = setup() as TestBed; + render(); - onFormData.mockReset(); // Reset the calls counter at 0 + onFormData.mockClear(); // Make some changes to a field we are **not** interested in - await act(async () => { - setInputValue('lastNameField', 'updated value'); - }); + const lastNameField = screen.getByTestId('lastNameField'); + await user.clear(lastNameField); + await user.type(lastNameField, 'updated value'); - expect(onFormData).toBeCalledTimes(0); + expect(onFormData).toHaveBeenCalledTimes(0); }); test('props.pathsToWatch (Array): should not re-render the children when the field that changed is not in the watch list', async () => { - const onFormData = jest.fn(); - const TestComp = () => { const { form } = useForm(); @@ -195,38 +164,35 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { - form: { setInputValue }, - } = setup() as TestBed; + render(); - onFormData.mockReset(); // Reset the calls counter at 0 + onFormData.mockClear(); // Make some changes to fields not in the watch list - await act(async () => { - setInputValue('companyField', 'updated value'); - }); + const companyField = screen.getByTestId('companyField'); + await user.clear(companyField); + await user.type(companyField, 'updated value'); // No re-render expect(onFormData).toBeCalledTimes(0); // Make some changes to fields in the watch list - await act(async () => { - setInputValue('nameField', 'updated value'); - }); + const nameField = screen.getByTestId('nameField'); + await user.clear(nameField); + await user.type(nameField, 'updated value'); - expect(onFormData).toBeCalledTimes(1); + // userEvent.type() triggers onChange for each character + expect(onFormData).toHaveBeenCalled(); - onFormData.mockReset(); + onFormData.mockClear(); - await act(async () => { - setInputValue('lastNameField', 'updated value'); - }); + const lastNameField = screen.getByTestId('lastNameField'); + await user.clear(lastNameField); + await user.type(lastNameField, 'updated value'); - expect(onFormData.mock.calls.length).toBe(2); // 2 as the form "isValid" change caused a re-render + // userEvent.type() triggers onChange for each character + // The form "isValid" change also causes a re-render + expect(onFormData).toHaveBeenCalled(); const [formData] = onFormData.mock.calls[ onFormData.mock.calls.length - 1 diff --git a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx index 8028bd1e082ed..d5f214568b12d 100644 --- a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx +++ b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_array.test.tsx @@ -8,18 +8,21 @@ */ import React, { useEffect } from 'react'; -import { act } from 'react-dom/test-utils'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import { registerTestBed } from '../shared_imports'; import { useForm } from '../hooks/use_form'; import { useFormData } from '../hooks/use_form_data'; import { Form } from './form'; import { UseField } from './use_field'; import { UseArray } from './use_array'; +const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + describe('', () => { beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); + jest.clearAllMocks(); }); afterAll(() => { @@ -50,13 +53,10 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { find } = setup(); + render(); - expect(find('arrayItem').length).toBe(1); + const items = screen.getAllByTestId('arrayItem'); + expect(items).toHaveLength(1); }); test('it should allow to listen to array item field value change', async () => { @@ -89,18 +89,10 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - defaultProps: { onData: onFormData }, - memoryRouter: { wrapComponent: false }, - }); - - const { - form: { setInputValue }, - } = setup(); + render(); - await act(async () => { - setInputValue('users[0]Name', 'John'); - }); + const nameField = await screen.findByTestId('users[0]Name'); + await user.type(nameField, 'John'); const formData = onFormData.mock.calls[onFormData.mock.calls.length - 1][0]; diff --git a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index 1b8d701826f67..affb668ef1a38 100644 --- a/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/platform/plugins/shared/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -9,30 +9,29 @@ import type { FunctionComponent } from 'react'; import React, { useEffect, useState, useCallback } from 'react'; -import { act } from 'react-dom/test-utils'; +import { render, screen, act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { first } from 'rxjs'; - -import type { TestBed } from '../shared_imports'; -import { registerTestBed } from '../shared_imports'; import type { FormHook, OnUpdateHandler, FieldConfig, FieldHook } from '../types'; import { useForm } from '../hooks/use_form'; import { useBehaviorSubject } from '../hooks/utils/use_behavior_subject'; import { Form } from './form'; import { UseField } from './use_field'; -describe('', () => { - beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); - }); +const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); - afterAll(() => { - jest.useRealTimers(); - }); +beforeAll(() => { + jest.useFakeTimers(); +}); +afterAll(() => { + jest.useRealTimers(); +}); + +describe('', () => { describe('defaultValue', () => { test('should read the default value from the prop and fallback to the config object', () => { const onFormData = jest.fn(); - const TestComp = ({ onData }: { onData: OnUpdateHandler }) => { const { form } = useForm(); const { subscribe } = form; @@ -51,24 +50,23 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - defaultProps: { onData: onFormData }, - memoryRouter: { wrapComponent: false }, - }); + render(); - setup(); + expect(onFormData).toHaveBeenCalledTimes(1); - const [{ data }] = onFormData.mock.calls[ - onFormData.mock.calls.length - 1 - ] as Parameters; - - expect(data.internal).toEqual({ - name: 'John', - lastName: 'Snow', - }); + expect(onFormData).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + internal: { + name: 'John', + lastName: 'Snow', + }, + }), + }) + ); }); - test('should update the form.defaultValue when a field defaultValue is provided through prop', () => { + test('should update the form.defaultValue when a field defaultValue is provided through prop', async () => { let formHook: FormHook | null = null; const TestComp = () => { @@ -94,11 +92,7 @@ describe('', () => { ); }; - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); - - const { find } = setup(); + render(); expect(formHook!.__getFormDefaultValue()).toEqual({ name: 'John', @@ -109,9 +103,8 @@ describe('', () => { }); // Unmounts the field and make sure the form.defaultValue has been updated - act(() => { - find('unmountField').simulate('click'); - }); + const unmountButton = screen.getByTestId('unmountField'); + await user.click(unmountButton); expect(formHook!.__getFormDefaultValue()).toEqual({}); }); @@ -177,11 +170,24 @@ describe('', () => { const toString = (value: unknown): string => typeof value === 'string' ? value : JSON.stringify(value); - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - }); + const setup = async (props: Props) => { + const renderResult = render(); + return { + ...renderResult, + user, + form: { + setInputValue: async (testId: string, value: string) => { + const input = screen.getByTestId(testId); + await user.clear(input); + if (value) { + await user.type(input, value); + } + }, + }, + }; + }; - [ + test.each([ { description: 'should update the state for field without default values', initialValue: '', @@ -218,38 +224,42 @@ describe('', () => { defaultValue: { initial: 'value' }, }, }, - ].forEach(({ description, fieldProps, initialValue, changedValue }) => { - test(description, async () => { - const { form } = await setup({ fieldProps }); - - expect(lastFieldState()).toEqual({ - isPristine: true, - isDirty: false, - isModified: false, - value: initialValue, - }); - - await act(async () => { - form.setInputValue('testField', toString(changedValue)); - }); - - expect(lastFieldState()).toEqual({ - isPristine: false, - isDirty: true, - isModified: true, - value: changedValue, - }); - - // Put back to the initial value --> isModified should be false - await act(async () => { - form.setInputValue('testField', toString(initialValue)); - }); - expect(lastFieldState()).toEqual({ - isPristine: false, - isDirty: true, - isModified: false, - value: initialValue, - }); + ])('$description', async ({ fieldProps, initialValue, changedValue }) => { + await setup({ fieldProps }); + + expect(lastFieldState()).toEqual({ + isPristine: true, + isDirty: false, + isModified: false, + value: initialValue, + }); + + const testField = screen.getByTestId('testField'); + // For object fields, select all + paste to avoid triggering onChange with empty string + await user.click(testField); + await user.keyboard('{Control>}a{/Control}'); + await user.paste(toString(changedValue)); + + expect(lastFieldState()).toEqual({ + isPristine: false, + isDirty: true, + isModified: true, + value: changedValue, + }); + + // Put back to the initial value --> isModified should be false + if (toString(initialValue)) { + await user.keyboard('{Control>}a{/Control}'); + await user.paste(toString(initialValue)); + } else { + // For empty initial value, clear the field + await user.clear(testField); + } + expect(lastFieldState()).toEqual({ + isPristine: false, + isDirty: true, + isModified: false, + value: initialValue, }); }); }); @@ -316,9 +326,20 @@ describe('', () => { }; const setup = (fieldConfig?: FieldConfig) => { - return registerTestBed(getTestComp(fieldConfig), { - memoryRouter: { wrapComponent: false }, - })() as TestBed; + const TestComp = getTestComp(fieldConfig); + render(); + return { + user, + form: { + setInputValue: async (testId: string, value: string) => { + const input = screen.getByTestId(testId); + await user.clear(input); + if (value) { + await user.type(input, value); + } + }, + }, + }; }; test('should update the form validity whenever the field value changes', async () => { @@ -350,7 +371,7 @@ describe('', () => { await act(async () => { const validatePromise = formHook!.validate(); // ...until we validate the form - jest.advanceTimersByTime(0); + await jest.runAllTimersAsync(); await validatePromise; }); @@ -359,7 +380,9 @@ describe('', () => { // Change to a non empty string to pass validation await act(async () => { - setInputValue('myField', 'changedValue'); + const setInputValuePromise = setInputValue('myField', 'changedValue'); + await jest.runAllTimersAsync(); + await setInputValuePromise; }); ({ isValid } = formHook); @@ -367,7 +390,9 @@ describe('', () => { // Change back to an empty string to fail validation await act(async () => { - setInputValue('myField', ''); + const setInputValuePromise = setInputValue('myField', ''); + await jest.runAllTimersAsync(); + await setInputValuePromise; }); ({ isValid } = formHook); @@ -391,7 +416,6 @@ describe('', () => { }; const { - find, form: { setInputValue }, } = setup(fieldConfig); @@ -399,30 +423,25 @@ describe('', () => { // Trigger validation... await act(async () => { - setInputValue('myField', 'changedValue'); + const setIinputValuePromise = setInputValue('myField', 'changedValue'); + await jest.advanceTimersToNextTimerAsync(0); + await setIinputValuePromise; }); expect(fieldHook?.isValidating).toBe(true); - // Unmount the field - await act(async () => { - find('unmountFieldBtn').simulate('click'); - }); - const originalConsoleError = console.error; // eslint-disable-line no-console const spyConsoleError = jest.fn((message) => { originalConsoleError(message); }); console.error = spyConsoleError; // eslint-disable-line no-console - // Move the timer to resolve the validator - await act(async () => { - jest.advanceTimersByTime(5000); - }); + const unmountBtn = screen.getByTestId('unmountFieldBtn'); + await user.click(unmountBtn); // The test should not display any warning // "Can't perform a React state update on an unmounted component." - expect(spyConsoleError.mock.calls.length).toBe(0); + expect(spyConsoleError).not.toHaveBeenCalled(); console.error = originalConsoleError; // eslint-disable-line no-console }); @@ -442,7 +461,6 @@ describe('', () => { }; const { - find, form: { setInputValue }, } = setup(fieldConfig); @@ -450,12 +468,14 @@ describe('', () => { setInputValue('myField', 'changedValue'); }); - expect(validator).toHaveBeenCalledTimes(1); + // userEvent.type triggers validation for each character typed + expect(validator).toHaveBeenCalled(); validator.mockReset(); await act(async () => { // Change the field path - find('changeFieldPathBtn').simulate('click'); + const changePathBtn = screen.getByTestId('changeFieldPathBtn'); + await user.click(changePathBtn); }); expect(validator).not.toHaveBeenCalled(); @@ -578,10 +598,28 @@ describe('', () => { }; const setupDynamicData = (defaultProps?: Partial) => { - return registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - defaultProps, - })() as TestBed; + const { unmount } = render(); + return { + unmount, + user, + form: { + setInputValue: async (testId: string, value: string) => { + const input = screen.getByTestId(testId); + await user.clear(input); + if (value) { + await user.type(input, value); + } + }, + }, + find: (testId: string) => ({ + simulate: async (event: string) => { + if (event === 'click') { + const button = screen.getByTestId(testId); + await user.click(button); + } + }, + }), + }; }; beforeEach(() => { @@ -592,7 +630,9 @@ describe('', () => { const { form, find } = setupDynamicData(); await act(async () => { - form.setInputValue('nameField', 'newValue'); + const inputValuePromise = form.setInputValue('nameField', 'newValue'); + await jest.runAllTimersAsync(); + await inputValuePromise; }); // If the field is validating this will prevent the form from being submitted as // it will wait for all the fields to finish validating to return the form validity. @@ -601,49 +641,54 @@ describe('', () => { // Let's wait 10 sec to make sure the validation does not complete // until the observable receives a value await act(async () => { - jest.advanceTimersByTime(10000); + await jest.advanceTimersByTimeAsync(10000); }); // The field is still validating as the validationDataProvider has not resolved yet // (no value has been sent to the observable) expect(nameFieldHook?.isValidating).toBe(true); // We now send a valid value to the observable - await act(async () => { - find('setValidValueBtn').simulate('click'); - }); + await find('setValidValueBtn').simulate('click'); expect(nameFieldHook?.isValidating).toBe(false); expect(nameFieldHook?.isValid).toBe(true); // Let's change the input value to trigger the validation once more await act(async () => { - form.setInputValue('nameField', 'anotherValue'); + const inputValuePromise = form.setInputValue('nameField', 'anotherValue'); + await jest.runAllTimersAsync(); + await inputValuePromise; }); expect(nameFieldHook?.isValidating).toBe(true); // And send an invalid value to the observable - await act(async () => { - find('setInvalidValueBtn').simulate('click'); - }); + await find('setInvalidValueBtn').simulate('click'); expect(nameFieldHook?.isValidating).toBe(false); expect(nameFieldHook?.isValid).toBe(false); expect(nameFieldHook?.getErrorsMessages()).toBe('Invalid dynamic data'); }); test('it should access dynamic data provided through props', async () => { - let { form } = setupDynamicData({ validationData: 'good' }); + let result = setupDynamicData({ validationData: 'good' }); await act(async () => { - form.setInputValue('lastNameField', 'newValue'); + const setInputValuePromise = result.form.setInputValue('lastNameField', 'newValue'); + await jest.runAllTimersAsync(); + await setInputValuePromise; }); // As this is a sync validation it should not be validating anymore at this stage expect(lastNameFieldHook?.isValidating).toBe(false); expect(lastNameFieldHook?.isValid).toBe(true); + // Cleanup before re-rendering + result.unmount(); + // Now let's provide invalid dynamic data through props - ({ form } = setupDynamicData({ validationData: 'bad' })); + result = setupDynamicData({ validationData: 'bad' }); await act(async () => { - form.setInputValue('lastNameField', 'newValue'); + const setInputValuePromise = result.form.setInputValue('lastNameField', 'newValue'); + await jest.runAllTimersAsync(); + await setInputValuePromise; }); expect(lastNameFieldHook?.isValidating).toBe(false); expect(lastNameFieldHook?.isValid).toBe(false); @@ -696,12 +741,7 @@ describe('', () => { }; test('should call each handler at expected lifecycle', async () => { - const setup = registerTestBed(TestComp, { - memoryRouter: { wrapComponent: false }, - defaultProps: { onForm: onFormHook }, - }); - - const testBed = setup() as TestBed; + render(); if (!formHook) { throw new Error( @@ -709,8 +749,6 @@ describe('', () => { ); } - const { form } = testBed; - expect(deserializer).toBeCalled(); expect(serializer).not.toBeCalled(); expect(formatter).not.toBeCalled(); @@ -718,9 +756,9 @@ describe('', () => { const internalFormData = formHook.__getFormData$().value; expect(internalFormData.name).toEqual('John-deserialized'); - await act(async () => { - form.setInputValue('myField', 'Mike'); - }); + const myField = screen.getByTestId('myField'); + await user.clear(myField); + await user.type(myField, 'Mike'); expect(formatter).toBeCalled(); // Formatters are executed on each value change expect(serializer).not.toBeCalled(); // Serializer are executed *only** when outputting the form data @@ -732,9 +770,15 @@ describe('', () => { // Make sure that when we reset the form values, we don't serialize the fields serializer.mockReset(); - await act(async () => { + act(() => { formHook!.reset(); }); + + await act(async () => { + // Wait for the form to reset + await jest.runAllTimersAsync(); + }); + expect(serializer).not.toBeCalled(); }); }); @@ -776,25 +820,17 @@ describe('', () => { it('allows function components', () => { const Component = () =>