From a8b4abbe12524a54e6399e03e2a20bf5e065392e Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 18 Jan 2022 16:21:58 +0100 Subject: [PATCH 1/6] Fix act warnings after test finishes This is a workaround for addressing the act warnings that might caused by store updates executed after the test finishes. --- test/native/helpers.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/test/native/helpers.js b/test/native/helpers.js index 487c8c7f83bc42..b59d0f35f79b02 100644 --- a/test/native/helpers.js +++ b/test/native/helpers.js @@ -35,7 +35,7 @@ provideToNativeHtml.mockImplementation( ( html ) => { } ); export function initializeEditor( props ) { - const renderResult = render( + const screen = render( ); - const { getByTestId } = renderResult; + const { getByTestId } = screen; // A promise is used here, instead of making the function async, to prevent // the React Native testing library from warning of potential undesired React state updates // that can be covered in the integration tests. // Reference: https://git.io/JPHn6 return new Promise( ( resolve ) => { - waitFor( () => getByTestId( 'block-list-wrapper' ) ).then( - ( blockListWrapper ) => { - // onLayout event has to be explicitly dispatched in BlockList component, - // otherwise the inner blocks are not rendered. - fireEvent( blockListWrapper, 'layout', { - nativeEvent: { - layout: { - width: 100, - }, + // Some of the store updates that happen upon editor initialization are executed at the end of the current + // Javascript block execution and after the test is finished. In order to prevent "act" warnings due to + // this behavior, we wait for the execution block to be finished before acting on the test. + act( + () => new Promise( ( actResolve ) => setImmediate( actResolve ) ) + ).then( () => { + // onLayout event has to be explicitly dispatched in BlockList component, + // otherwise the inner blocks are not rendered. + fireEvent( getByTestId( 'block-list-wrapper' ), 'layout', { + nativeEvent: { + layout: { + width: 100, }, - } ); - resolve( renderResult ); - } - ); + }, + } ); + + resolve( screen ); + } ); } ); } From 6678a204feb9dc13ddf1ab328a5be1c14cd57374 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 19 Jan 2022 10:00:21 +0100 Subject: [PATCH 2/6] Update comment of setImmediate workaround Co-authored-by: David Calhoun <438664+dcalhoun@users.noreply.github.com> --- test/native/helpers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/native/helpers.js b/test/native/helpers.js index b59d0f35f79b02..585d4c4a3b7d41 100644 --- a/test/native/helpers.js +++ b/test/native/helpers.js @@ -50,9 +50,10 @@ export function initializeEditor( props ) { // that can be covered in the integration tests. // Reference: https://git.io/JPHn6 return new Promise( ( resolve ) => { - // Some of the store updates that happen upon editor initialization are executed at the end of the current - // Javascript block execution and after the test is finished. In order to prevent "act" warnings due to - // this behavior, we wait for the execution block to be finished before acting on the test. + // During editor initialization, asynchronous store resolvers rely upon `setTimeout` to run at the end + // of the current JavaScript block execution. In order to prevent "act" warnings triggered by updates + // to the React tree, we leverage `setImmediate` to await the resolution of the current block execution + // before proceeding. act( () => new Promise( ( actResolve ) => setImmediate( actResolve ) ) ).then( () => { From af1d8cab0d08ad40c2cff105b5e15460538792eb Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 19 Jan 2022 14:13:22 +0100 Subject: [PATCH 3/6] Unmount editor when test finish --- packages/rich-text/src/test/index.native.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rich-text/src/test/index.native.js b/packages/rich-text/src/test/index.native.js index f91e2e18b4bc01..64e3a0447fadc8 100644 --- a/packages/rich-text/src/test/index.native.js +++ b/packages/rich-text/src/test/index.native.js @@ -231,9 +231,10 @@ describe( '', () => {

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet ut nibh vitae ornare. Sed auctor nec augue at blandit.

`; // Act - await initializeEditor( { initialHtml } ); + const { unmount } = await initializeEditor( { initialHtml } ); // Assert expect( getEditorHtml() ).toMatchSnapshot(); + unmount(); } ); it( 'should update the font size with decimals when style prop with font size property is provided', () => { From e6ef33d71df1d1debd926ad87fe5d86530584fd6 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 19 Jan 2022 14:15:03 +0100 Subject: [PATCH 4/6] Update initialize editor helper The function is now synchronous as we don't have to wait for any element. --- test/native/helpers.js | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/test/native/helpers.js b/test/native/helpers.js index b59d0f35f79b02..6b98520cb3956f 100644 --- a/test/native/helpers.js +++ b/test/native/helpers.js @@ -45,30 +45,17 @@ export function initializeEditor( props ) { ); const { getByTestId } = screen; - // A promise is used here, instead of making the function async, to prevent - // the React Native testing library from warning of potential undesired React state updates - // that can be covered in the integration tests. - // Reference: https://git.io/JPHn6 - return new Promise( ( resolve ) => { - // Some of the store updates that happen upon editor initialization are executed at the end of the current - // Javascript block execution and after the test is finished. In order to prevent "act" warnings due to - // this behavior, we wait for the execution block to be finished before acting on the test. - act( - () => new Promise( ( actResolve ) => setImmediate( actResolve ) ) - ).then( () => { - // onLayout event has to be explicitly dispatched in BlockList component, - // otherwise the inner blocks are not rendered. - fireEvent( getByTestId( 'block-list-wrapper' ), 'layout', { - nativeEvent: { - layout: { - width: 100, - }, - }, - } ); - - resolve( screen ); - } ); + // onLayout event has to be explicitly dispatched in BlockList component, + // otherwise the inner blocks are not rendered. + fireEvent( getByTestId( 'block-list-wrapper' ), 'layout', { + nativeEvent: { + layout: { + width: 100, + }, + }, } ); + + return screen; } export * from '@testing-library/react-native'; From a7d9acc8926d1384082758311bb4da30f1adcf6f Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 19 Jan 2022 14:16:21 +0100 Subject: [PATCH 5/6] Update tests that use initialize editor --- .../code/react-native/integration-test-guide.md | 2 +- packages/block-library/src/block/test/edit.native.js | 12 ++++-------- .../block-library/src/buttons/test/edit.native.js | 4 ++-- packages/block-library/src/cover/test/edit.native.js | 12 ++++-------- .../block-library/src/embed/test/index.native.js | 12 ++++++------ 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/docs/contributors/code/react-native/integration-test-guide.md b/docs/contributors/code/react-native/integration-test-guide.md index 14cb479dae1948..b0bddd37b9e815 100644 --- a/docs/contributors/code/react-native/integration-test-guide.md +++ b/docs/contributors/code/react-native/integration-test-guide.md @@ -89,7 +89,7 @@ const initialHtml = ` `; -const { getByA11yLabel } = await initializeEditor( { +const { getByA11yLabel } = initializeEditor( { initialHtml, } ); ``` diff --git a/packages/block-library/src/block/test/edit.native.js b/packages/block-library/src/block/test/edit.native.js index 4c4ef6e72a5247..58a4fad8d84f77 100644 --- a/packages/block-library/src/block/test/edit.native.js +++ b/packages/block-library/src/block/test/edit.native.js @@ -74,11 +74,7 @@ describe( 'Reusable block', () => { return Promise.resolve( response ); } ); - const { - getByA11yLabel, - getByTestId, - getByText, - } = await initializeEditor( { + const { getByA11yLabel, getByTestId, getByText } = initializeEditor( { initialHtml: '', capabilities: { reusableBlock: true }, } ); @@ -87,7 +83,7 @@ describe( 'Reusable block', () => { fireEvent.press( await waitFor( () => getByA11yLabel( 'Add block' ) ) ); // Navigate to reusable tab - const reusableSegment = getByText( 'Reusable' ); + const reusableSegment = await waitFor( () => getByText( 'Reusable' ) ); // onLayout event is required by Segment component fireEvent( reusableSegment, 'layout', { nativeEvent: { @@ -127,7 +123,7 @@ describe( 'Reusable block', () => { const id = 3; const initialHtml = ``; - const { getByA11yLabel } = await initializeEditor( { + const { getByA11yLabel } = initializeEditor( { initialHtml, } ); @@ -162,7 +158,7 @@ describe( 'Reusable block', () => { return Promise.resolve( response ); } ); - const { getByA11yLabel } = await initializeEditor( { + const { getByA11yLabel } = initializeEditor( { initialHtml, } ); diff --git a/packages/block-library/src/buttons/test/edit.native.js b/packages/block-library/src/buttons/test/edit.native.js index a9ea061a9f6e7b..ab761b12206016 100644 --- a/packages/block-library/src/buttons/test/edit.native.js +++ b/packages/block-library/src/buttons/test/edit.native.js @@ -35,7 +35,7 @@ describe( 'Buttons block', () => { `; - const { getByA11yLabel } = await initializeEditor( { + const { getByA11yLabel } = initializeEditor( { initialHtml, } ); @@ -90,7 +90,7 @@ describe( 'Buttons block', () => { const initialHtml = `
`; - const { getByA11yLabel, getByText } = await initializeEditor( { + const { getByA11yLabel, getByText } = initializeEditor( { initialHtml, } ); diff --git a/packages/block-library/src/cover/test/edit.native.js b/packages/block-library/src/cover/test/edit.native.js index c09b21f3eb8ff3..a6248cbb8f9e19 100644 --- a/packages/block-library/src/cover/test/edit.native.js +++ b/packages/block-library/src/cover/test/edit.native.js @@ -295,7 +295,7 @@ describe( 'when an image is attached', () => { describe( 'color settings', () => { it( 'sets a color for the overlay background when the placeholder is visible', async () => { - const { getByTestId, getByA11yLabel } = await initializeEditor( { + const { getByTestId, getByA11yLabel } = initializeEditor( { initialHtml: COVER_BLOCK_PLACEHOLDER_HTML, } ); @@ -350,7 +350,7 @@ describe( 'color settings', () => { } ); it( 'sets a gradient overlay background when a solid background was already selected', async () => { - const { getByTestId, getByA11yLabel } = await initializeEditor( { + const { getByTestId, getByA11yLabel } = initializeEditor( { initialHtml: COVER_BLOCK_SOLID_COLOR_HTML, } ); @@ -407,7 +407,7 @@ describe( 'color settings', () => { } ); it( 'toggles between solid colors and gradients', async () => { - const { getByTestId, getByA11yLabel } = await initializeEditor( { + const { getByTestId, getByA11yLabel } = initializeEditor( { initialHtml: COVER_BLOCK_PLACEHOLDER_HTML, } ); @@ -493,11 +493,7 @@ describe( 'color settings', () => { } ); it( 'clears the selected overlay color and mantains the inner blocks', async () => { - const { - getByTestId, - getByA11yLabel, - getByText, - } = await initializeEditor( { + const { getByTestId, getByA11yLabel, getByText } = initializeEditor( { initialHtml: COVER_BLOCK_SOLID_COLOR_HTML, } ); diff --git a/packages/block-library/src/embed/test/index.native.js b/packages/block-library/src/embed/test/index.native.js index 346d88fec35f23..39e4f0270ca840 100644 --- a/packages/block-library/src/embed/test/index.native.js +++ b/packages/block-library/src/embed/test/index.native.js @@ -142,7 +142,7 @@ const mockEmbedResponses = ( mockedResponses ) => { }; const insertEmbedBlock = async ( blockTitle = 'Embed' ) => { - const editor = await initializeEditor( { + const editor = initializeEditor( { initialHtml: '', } ); const { getByA11yLabel, getByText } = editor; @@ -162,7 +162,7 @@ const insertEmbedBlock = async ( blockTitle = 'Embed' ) => { }; const initializeWithEmbedBlock = async ( initialHtml, selectBlock = true ) => { - const editor = await initializeEditor( { initialHtml } ); + const editor = initializeEditor( { initialHtml } ); const { getByA11yLabel } = editor; const block = await waitFor( () => @@ -871,7 +871,7 @@ describe( 'Embed block', () => { getByPlaceholderText, getByTestId, getByText, - } = await initializeEditor( { + } = initializeEditor( { initialHtml: EMPTY_PARAGRAPH_HTML, } ); @@ -914,7 +914,7 @@ describe( 'Embed block', () => { getByPlaceholderText, getByTestId, getByText, - } = await initializeEditor( { + } = initializeEditor( { initialHtml: EMPTY_PARAGRAPH_HTML, } ); @@ -959,7 +959,7 @@ describe( 'Embed block', () => { getByPlaceholderText, getByA11yLabel, getByText, - } = await initializeEditor( { initialHtml: EMPTY_PARAGRAPH_HTML } ); + } = initializeEditor( { initialHtml: EMPTY_PARAGRAPH_HTML } ); const paragraphText = getByPlaceholderText( 'Start writing…' ); fireEvent( paragraphText, 'focus' ); @@ -1001,7 +1001,7 @@ describe( 'Embed block', () => { getByPlaceholderText, getByA11yLabel, getByText, - } = await initializeEditor( { + } = initializeEditor( { initialHtml: EMPTY_PARAGRAPH_HTML, } ); From 02848bdad76c544f03ae1c47f640033a36ef6fe2 Mon Sep 17 00:00:00 2001 From: David Calhoun <438664+dcalhoun@users.noreply.github.com> Date: Wed, 19 Jan 2022 10:03:22 -0600 Subject: [PATCH 6/6] Fix act warnings from store resolvers with fake timers (#38077) * Leverage fake timers to resolve store resolvers During editor initialization, asynchronous store resolvers rely upon `setTimeout` to run at the end of the current JavaScript block execution. In order to prevent "act" warnings triggered by updates to the React tree, we leverage fake timers to manually tick and await the resolution of the current block execution before proceeding. * Update RichText test `initializeEditor` usage The refactor of `initializeEditor` to leverage fake timers means this test no longer needs to `await` asynchronous work or manuall `unmount` its subject component. * Refactor usage of `initializeEditor` in tests Now that `initializeEditor` leverages fake timers to resolve asynchronous store resolvers, the tests no longer need to await a resolution of `initializeEditor`. * Fix `act` warnings in Image edit test Retrieving text from the clipboard is an asynchronous action. The component updated React state once the clipboard resolved. This resulted in a state update triggering an `act` warning. Because there is no visible change to the rendered output, e.g. updated text or UI, we must await the resolution of the clipboard promise itself. --- .../src/image/test/edit.native.js | 24 +++++++++----- .../missing/test/edit-integration.native.js | 4 +-- .../mobile/link-settings/test/edit.native.js | 10 +++--- .../src/text-color/test/index.native.js | 8 ++--- packages/rich-text/src/test/index.native.js | 5 ++- test/native/helpers.js | 31 +++++++++++++++++-- 6 files changed, 58 insertions(+), 24 deletions(-) diff --git a/packages/block-library/src/image/test/edit.native.js b/packages/block-library/src/image/test/edit.native.js index 1ae1ed5291e336..779c78082fa987 100644 --- a/packages/block-library/src/image/test/edit.native.js +++ b/packages/block-library/src/image/test/edit.native.js @@ -3,6 +3,7 @@ */ import { act, fireEvent, initializeEditor, getEditorHtml } from 'test/helpers'; import { Image } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; /** * WordPress dependencies @@ -36,6 +37,9 @@ jest.mock( 'lodash', () => { const apiFetchPromise = Promise.resolve( {} ); apiFetch.mockImplementation( () => apiFetchPromise ); +const clipboardPromise = Promise.resolve( '' ); +Clipboard.getString.mockImplementation( () => clipboardPromise ); + beforeAll( () => { registerCoreBlocks(); @@ -63,7 +67,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -89,7 +93,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -115,7 +119,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -127,6 +131,8 @@ describe( 'Image Block', () => { ); fireEvent.press( screen.getByText( 'None' ) ); fireEvent.press( screen.getByText( 'Custom URL' ) ); + // Await asynchronous fetch of clipboard + await act( () => clipboardPromise ); fireEvent.changeText( screen.getByPlaceholderText( 'Search or type URL' ), 'wordpress.org' @@ -146,7 +152,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -159,12 +165,16 @@ describe( 'Image Block', () => { fireEvent.press( screen.getByText( 'None' ) ); fireEvent.press( screen.getByText( 'Media File' ) ); fireEvent.press( screen.getByText( 'Custom URL' ) ); + // Await asynchronous fetch of clipboard + await act( () => clipboardPromise ); fireEvent.changeText( screen.getByPlaceholderText( 'Search or type URL' ), 'wordpress.org' ); fireEvent.press( screen.getByA11yLabel( 'Apply' ) ); fireEvent.press( screen.getByText( 'Custom URL' ) ); + // Await asynchronous fetch of clipboard + await act( () => clipboardPromise ); fireEvent.press( screen.getByText( 'Media File' ) ); const expectedHtml = ` @@ -182,7 +192,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -206,7 +216,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); @@ -237,7 +247,7 @@ describe( 'Image Block', () => {
Mountain
`; - const screen = await initializeEditor( { initialHtml } ); + const screen = initializeEditor( { initialHtml } ); // We must await the image fetch via `getMedia` await act( () => apiFetchPromise ); diff --git a/packages/block-library/src/missing/test/edit-integration.native.js b/packages/block-library/src/missing/test/edit-integration.native.js index eab4fa1f6f304e..8d945f3a21e444 100644 --- a/packages/block-library/src/missing/test/edit-integration.native.js +++ b/packages/block-library/src/missing/test/edit-integration.native.js @@ -40,7 +40,7 @@ describe( 'Unsupported block', () => { const initialHtml = `
12
34
`; - const { getByA11yLabel } = await initializeEditor( { + const { getByA11yLabel } = initializeEditor( { initialHtml, } ); @@ -59,7 +59,7 @@ describe( 'Unsupported block', () => { const initialHtml = `
12
34
`; - const { getByA11yLabel, getByText } = await initializeEditor( { + const { getByA11yLabel, getByText } = initializeEditor( { initialHtml, } ); diff --git a/packages/components/src/mobile/link-settings/test/edit.native.js b/packages/components/src/mobile/link-settings/test/edit.native.js index 80aff4e7401a68..ec069f303d3ca8 100644 --- a/packages/components/src/mobile/link-settings/test/edit.native.js +++ b/packages/components/src/mobile/link-settings/test/edit.native.js @@ -72,7 +72,7 @@ describe.each( [ it( 'should display the LINK SETTINGS with an EMPTY LINK TO field.', async () => { // Arrange const url = 'https://tonytahmouchtest.files.wordpress.com'; - const subject = await initializeEditor( { initialHtml } ); + const subject = initializeEditor( { initialHtml } ); Clipboard.getString.mockReturnValue( url ); // Act @@ -109,7 +109,7 @@ describe.each( [ it( 'should display the LINK PICKER with NO FROM CLIPBOARD CELL.', async () => { // Arrange const url = 'tonytahmouchtest.files.wordpress.com'; - const subject = await initializeEditor( { initialHtml } ); + const subject = initializeEditor( { initialHtml } ); Clipboard.getString.mockReturnValue( url ); // Act @@ -162,7 +162,7 @@ describe.each( [ it( 'should display the LINK PICKER with NO FROM CLIPBOARD CELL.', async () => { // Arrange const url = 'https://tonytahmouchtest.files.wordpress.com'; - const subject = await initializeEditor( { initialHtml } ); + const subject = initializeEditor( { initialHtml } ); Clipboard.getString.mockReturnValue( url ); // Act @@ -241,7 +241,7 @@ describe.each( [ async () => { // Arrange const url = 'https://tonytahmouchtest.files.wordpress.com'; - const subject = await initializeEditor( { initialHtml } ); + const subject = initializeEditor( { initialHtml } ); Clipboard.getString.mockReturnValue( url ); // Act @@ -308,7 +308,7 @@ describe.each( [ async () => { // Arrange const url = 'https://tonytahmouchtest.files.wordpress.com'; - const subject = await initializeEditor( { initialHtml } ); + const subject = initializeEditor( { initialHtml } ); Clipboard.getString.mockReturnValue( url ); // Act diff --git a/packages/format-library/src/text-color/test/index.native.js b/packages/format-library/src/text-color/test/index.native.js index 742fb52822235e..6b48ecc3d0bfbd 100644 --- a/packages/format-library/src/text-color/test/index.native.js +++ b/packages/format-library/src/text-color/test/index.native.js @@ -32,7 +32,7 @@ afterAll( () => { describe( 'Text color', () => { it( 'shows the text color formatting button in the toolbar', async () => { - const { getByA11yLabel } = await initializeEditor(); + const { getByA11yLabel } = initializeEditor(); // Wait for the editor placeholder const paragraphPlaceholder = await waitFor( () => @@ -59,7 +59,7 @@ describe( 'Text color', () => { getByA11yLabel, getByTestId, getByA11yHint, - } = await initializeEditor(); + } = initializeEditor(); // Wait for the editor placeholder const paragraphPlaceholder = await waitFor( () => @@ -101,7 +101,7 @@ describe( 'Text color', () => { getByTestId, getByPlaceholderText, getByA11yHint, - } = await initializeEditor(); + } = initializeEditor(); const text = 'Hello this is a test'; // Wait for the editor placeholder @@ -149,7 +149,7 @@ describe( 'Text color', () => { } ); it( 'creates a paragraph block with the text color format', async () => { - const { getByA11yLabel } = await initializeEditor( { + const { getByA11yLabel } = initializeEditor( { initialHtml: TEXT_WITH_COLOR, } ); diff --git a/packages/rich-text/src/test/index.native.js b/packages/rich-text/src/test/index.native.js index 64e3a0447fadc8..de926297360857 100644 --- a/packages/rich-text/src/test/index.native.js +++ b/packages/rich-text/src/test/index.native.js @@ -225,16 +225,15 @@ describe( '', () => { expect( screen.toJSON() ).toMatchSnapshot(); } ); - it( 'renders component with style and font size', async () => { + it( 'renders component with style and font size', () => { // Arrange const initialHtml = `

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet ut nibh vitae ornare. Sed auctor nec augue at blandit.

`; // Act - const { unmount } = await initializeEditor( { initialHtml } ); + initializeEditor( { initialHtml } ); // Assert expect( getEditorHtml() ).toMatchSnapshot(); - unmount(); } ); it( 'should update the font size with decimals when style prop with font size property is provided', () => { diff --git a/test/native/helpers.js b/test/native/helpers.js index 6b98520cb3956f..22a0c7bb56f253 100644 --- a/test/native/helpers.js +++ b/test/native/helpers.js @@ -35,6 +35,20 @@ provideToNativeHtml.mockImplementation( ( html ) => { } ); export function initializeEditor( props ) { + // Portions of the React Native Animation API rely upon these APIs. However, + // Jest's 'legacy' fake timer mutate these globals, which breaks the Animated + // API. We preserve the original implementations to restore them later. + const originalRAF = global.requestAnimationFrame; + const originalCAF = global.cancelAnimationFrame; + + // During editor initialization, asynchronous store resolvers rely upon + // `setTimeout` to run at the end of the current JavaScript block execution. + // In order to prevent "act" warnings triggered by updates to the React tree, + // we leverage fake timers to manually tick and await the resolution of the + // current block execution before proceeding. + jest.useFakeTimers( 'legacy' ); + + // Arrange const screen = render( ); - const { getByTestId } = screen; - // onLayout event has to be explicitly dispatched in BlockList component, + // Layout event must be explicitly dispatched in BlockList component, // otherwise the inner blocks are not rendered. - fireEvent( getByTestId( 'block-list-wrapper' ), 'layout', { + fireEvent( screen.getByTestId( 'block-list-wrapper' ), 'layout', { nativeEvent: { layout: { width: 100, @@ -55,6 +68,18 @@ export function initializeEditor( props ) { }, } ); + // Advance all timers allowing store resolvers to resolve. + act( () => jest.runAllTimers() ); + + // Restore the default timer APIs for remainder of test arrangement, act, and + // assertion. + jest.useRealTimers(); + + // Restore the global animation frame APIs to their original state for the + // React Native Animated API. + global.requestAnimationFrame = originalRAF; + global.cancelAnimationFrame = originalCAF; + return screen; }